截获对象的操作

我们先来看一个需求:有一个对象,我们希望截获这个对象中的属性被设置或获取的过程。

我们可以通过前面所学的 Object.defineProperty 的存取属性描述符来对属性的操作进行截获。

  1. const obj = {
  2. name: 'zs',
  3. age: 18
  4. }
  5. Object.keys(obj).forEach(key => {
  6. // 获取属性值
  7. let val = obj[key]
  8. // 截获属性的 set、get 操作
  9. Object.defineProperty(obj, key, {
  10. set: function(newVal) {
  11. console.log(key + '属性 set 操作被截获');
  12. val = newVal
  13. },
  14. get: function() {
  15. console.log(key + '属性 get 操作被截获');
  16. return val
  17. }
  18. })
  19. })
  20. console.log(obj.name);
  21. obj.age = 22
  22. console.log(obj.age);
  23. // name属性 get 操作被截获
  24. // zs
  25. // age属性 set 操作被截获
  26. // age属性 get 操作被截获
  27. // 22

但是这样做有什么缺点呢?

  • 首先,Object.defineProperty设计的初衷,不是为了去捕获截止一个对象中所有的属性的。我们在定义某些属性的时候,初衷其实是定义成数据属性描述符,但是后面我们强行将它变成了存取属性描述符。
  • 其次,如果我们想捕获更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty 是无能为力的。

这个截获的操作其实就是代理模式,所以 ES6 直接新增了 Proxy

Proxy 代理

在ES6中,新增了一个 Proxy 代理类,可以截获一些对象操作,对对象操作进行一些增强处理,并且也可以避免对对象的直接操作。
new Proxy(target, handler)

  • target:代理的目标对象
  • handler:一个处理对象,里面包含了 13个 捕获器方法

    Proxy的set和get捕获器

    我们想要侦听某些具体的操作,那么就可以在 handler 中添加对应的捕捉器(Trap):最常见的是 set、get
    set 和 get 分别对应的是函数类型;
    set 函数有四个参数:

  • target:目标对象(代理的对象);

  • property:将被设置的属性key;
  • value:新属性值;

receiver:调用的代理对象;

  • get函数有三个参数:
  • target:目标对象(代理的对象);
  • property:被获取的属性key;
  • receiver:调用的代理对象; ```javascript const obj = { name: ‘why’, age: 18 }

const objProxy = new Proxy(obj, { // 获取值时的捕获器 get: function (target, key) { // 捕获后的处理 console.log(捕获到对象的${key}属性被访问了, target) // 执行原本的 get 操作 return target[key] },

// 设置值时的捕获器 set: function (target, key, newValue) { console.log(捕获到对象的${key}属性被设置值, target) target[key] = newValue },

// 监听in的捕获器 has: function (target, key) { console.log(捕获到对象的${key}属性in操作, target) return key in target },

// 监听delete的捕获器 deleteProperty: function (target, key) { console.log(捕获到对象的${key}属性in操作, target) delete target[key] } })

// get 操作 console.log(objProxy.name)

// set 操作 objProxy.age = 22

// in操作符 console.log(‘name’ in objProxy)

// delete操作 delete objProxy.name

  1. <a name="J19oZ"></a>
  2. ## Proxy所有捕获器
  3. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22919157/1649864003579-7c25529a-807c-4277-b40b-5182f321c195.png#clientId=udaec0891-7161-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=454&id=u8611d516&margin=%5Bobject%20Object%5D&name=image.png&originHeight=568&originWidth=1053&originalType=binary&ratio=1&rotation=0&showTitle=false&size=163881&status=done&style=none&taskId=u0918db39-b185-4551-bead-f8dc4d8186b&title=&width=842.4)
  4. <a name="aM7jv"></a>
  5. ## Proxy 的 construct 和 apply 捕获器
  6. 其他的捕获器都是捕获普通对象操作的,constructor 和 apply 是函数才有的操作,所以这两个也是捕获函数对象的捕获器。
  7. ```javascript
  8. function foo() { }
  9. const fooProxy = new Proxy(foo, {
  10. apply: function(target, thisArg, argArray) { // thisArg:this argArray:参数数组
  11. // 捕获后的额外操作
  12. console.log("对foo函数进行了apply调用")
  13. // 原本的 apply 操作
  14. return target.apply(thisArg, argArray)
  15. },
  16. construct: function(target, argArray, newTarget) {
  17. console.log("对foo函数进行了new调用")
  18. return new target(...argArray)
  19. }
  20. })
  21. fooProxy.apply({}, ["abc", "cba"]) // 对foo函数进行了apply调用
  22. new fooProxy("abc", "cba") // 对foo函数进行了new调用

Reflect 反射

Reflect 是一个内置的对象,它主要提供了很多操作 JavaScript 对象的方法,有点像 Object 中操作对象的方法;Reflect不是一个函数对象,因此它是不可构造的。

操作对象的方法已经在 Object 中有了,为什么还需要 Reflect 来重复一次?
这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上。
换句话说 Reflect 就是来替代 Object 来对对象进行操作的

Reflect 的常见方法

Reflect 对象的常见方法和 proxy handlers 是一一对应的。
image.png

Reflect 的用法

一般是和 Proxy 连用。Proxy 之前的实现存在一些问题,代理的目的之一就是避免对对象的直接操作,但是在捕获器方法中类似target[key],还是对对象进行了直接操作。

  1. const obj = {
  2. name: 'zs',
  3. age: 18
  4. }
  5. const objProxy = new Proxy(obj, {
  6. get(target, key) {
  7. // return target[key] // 相当于 obj.name 直接对对象进行操作
  8. return Reflect.get(target, key) // 反射调用相当于又包裹了一层,避免了直接调用
  9. },
  10. set(target, key, newVal) {
  11. console.log('set 被执行');
  12. // 反射的 set 和普通的 set 不一样,会返回一个 Boolean 值,判断是否 set 成功
  13. Reflect.set(target, newVal)
  14. },
  15. has(target, key) {
  16. console.log(`存在${key}属性`);
  17. Reflect.has(target, key)
  18. },
  19. deleteProperty(target, key) {
  20. console.log(`${key}属性被删除`);
  21. Reflect.deleteProperty(target, key)
  22. }
  23. })
  24. console.log(objProxy.name); // zs
  25. objProxy.age = 22 // set 被执行
  26. console.log(objProxy.age); // 18
  27. 'name' in objProxy // 存在name属性
  28. delete objProxy.name // name属性被删除

Receiver 参数的作用

之前的代理反射的方式还有一个问题:对于私密属性中 setter、getter 里面访问其他属性的操作,会绕开代理对象的捕获。因为其中的 this 指向对象本身而不是代理对象。
Receiver 参数就是代理对象本身,将它传入 Reflect 的 get、set 方法,就可以修改 setter、getter this 的指向,使它指向代理对象。

  1. const obj = {
  2. name: 'zs',
  3. // 隐藏 _age 属性
  4. _age: 18,
  5. get age() {
  6. console.log(this);
  7. return this._age
  8. },
  9. // 普通方法
  10. sing: function() {
  11. console.log(this);
  12. console.log(this.name + ' singing'); // 方法中调用了其他属性
  13. }
  14. }
  15. const objProxy = new Proxy(obj, {
  16. get(target, key) {
  17. console.log('get-------');
  18. return Reflect.get(target, key)
  19. }
  20. })
  21. // 访问私密属性,调用 get,get 中访问对象属性不会被代理捕获
  22. console.log(objProxy.age);
  23. // get-------
  24. // Object {name: ...} 因为对象 get 操作中,this 指向obj,直接对name属性调用
  25. // 18
  26. // 普通方法属性
  27. objProxy.sing()
  28. // get------- 反射方法执行,访问 sing 属性被代理捕获
  29. // Proxy {}
  30. // get------- 执行 this.name 时,this 指向反射对象,访问 name 属性再被代理捕获一次
  31. // zs singing
  1. const obj = {
  2. name: 'zs',
  3. // 隐藏 _age 属性
  4. _age: 18,
  5. get age() {
  6. console.log(this);
  7. return this._age
  8. },
  9. // 普通方法
  10. sing: function() {
  11. console.log(this);
  12. console.log(this.name + ' singing');
  13. }
  14. }
  15. const objProxy = new Proxy(obj, {
  16. get(target, key, recevicer) { // 添加 recevicer 属性
  17. console.log('get-------');
  18. return Reflect.get(target, key, recevicer) // 添加 recevicer 属性
  19. }
  20. })
  21. console.log(objProxy.age);
  22. // get-------
  23. // Proxy {}
  24. // get-------
  25. // 18
  26. objProxy.sing()
  27. // get-------
  28. // Proxy {}
  29. // get-------
  30. // zs singing

Reflect 的 construct 方法

这个方法可以夺舍其他对象,拥有自身构造方法构造出来的灵魂,类型却是其他对象。

  1. function Student(name, age) {
  2. this.name = name
  3. this.age = age
  4. }
  5. function Teacher() {
  6. }
  7. // 一般情况下的对象
  8. const stu = new Student("why", 18)
  9. console.log(stu) // Student { name: 'why', age: 18 }
  10. console.log(stu.__proto__ === Student.prototype) // true
  11. // 夺舍 Teacher,执行Student函数中的内容, 但是创建出来对象是Teacher对象
  12. const teacher = Reflect.construct(Student, ["why", 18], Teacher)
  13. console.log(teacher) // Teacher { name: 'why', age: 18 }
  14. console.log(teacher.__proto__ === Teacher.prototype) // true

响应式原理

什么是响应式?

当变量或者对象变化时,使用了该变量、对象的代码或者一些相关的代码(也称为依赖),会自动使用最新的值重新执行一次,这就是响应式。
实际情况,一般针对对象的变化进行响应式处理。

  1. const obj = {
  2. name: 'zs',
  3. age: 18
  4. }
  5. // name 变化后,需要重新执行的代码
  6. console.log(obj.name)
  7. console.log(`与 name 属性相关的其他代码`);
  8. // name 变化
  9. obj.name = `ls`

响应式函数设计

首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中。那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数。

  1. const obj = {
  2. name: 'zs',
  3. age: 18
  4. }
  5. // 以函数包裹
  6. function foo() {
  7. // name 变化后,需要重新执行的代码
  8. console.log(obj.name)
  9. console.log(`与 name 属性相关的其他代码`)
  10. }
  11. // name 变化
  12. obj.name = `ls`
  13. // 响应式执行
  14. foo()

现在存在一个问题,系统中还有很多与 name 属性无关的函数,它们不需要响应式。该怎么区分它们,怎么实现让 name 值变化,只会执行 foo 函数,而不执行其他普通函数?
答案就是再包裹一层响应式函数。并且我们可以维护一个数组,数组来保存这些要响应式执行的函数,因为针对 name 变化的代码未必能全包裹在一个函数中。

  1. const obj = {
  2. name: 'zs',
  3. age: 18
  4. }
  5. // 以函数包裹
  6. function fName1() {
  7. // name 变化后,需要重新执行的代码
  8. console.log(obj.name)
  9. console.log(`与 name 属性相关的其他代码1`)
  10. }
  11. function fName2() {
  12. console.log(`与 name 属性相关的其他代码2`)
  13. }
  14. // 不需要响应式的普通函数
  15. function bar() {
  16. console.log(`阿巴阿巴阿巴,不需要响应式的代码`)
  17. }
  18. // 响应式函数
  19. const arrName = []
  20. function watchFn(fn) {
  21. arrName.push(fn)
  22. }
  23. watchFn(fName1)
  24. watchFn(fName2)
  25. // name 变化
  26. obj.name = `ls`
  27. // 响应式执行:遍历数组元素执行要响应式执行的函数
  28. arrName.forEach(item => {
  29. item()
  30. })

响应式依赖的收集

目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:如果有多个对象怎么办?对象中又有多个属性,为了保存它们的依赖,我们就得全局创建多个数组。这样太烦了。

我们可以设计一个类,这个类的实例化对象都拥有一个属于自己的数组,然后也有自己的实例方法可以将依赖的汇总函数放入数组中。
watchFn 函数中就可以使用这个实例方法,而不用多个对象和属性下,一个一个去创建数组,复用了代码。(别忘了类也是为复用代码而生)
其实现在 watchFn 函数有总多此一举的感觉,完全可以用 依赖对象直接调用 addDepend。不用再被 watchFn 包裹一层。

  1. class Depend {
  2. constructor() {
  3. this.reactiveFns = [] // 每个实例化对象都维护了一个数组
  4. }
  5. // 将依赖放入数组中
  6. addDepend(fn) {
  7. this.reactiveFns.push(fn)
  8. }
  9. // 还可以把响应式执行封装一下
  10. notify() {
  11. this.reactiveFns.forEach(fn => {
  12. fn()
  13. })
  14. }
  15. }
  16. const obj = {
  17. name: 'zs',
  18. age: 18
  19. }
  20. // 以函数包裹
  21. function fName1() {
  22. // name 变化后,需要重新执行的代码
  23. console.log(obj.name)
  24. console.log(`与 name 属性相关的其他代码1`)
  25. }
  26. function fName2() {
  27. console.log(`与 name 属性相关的其他代码2`)
  28. }
  29. // age 属性的依赖
  30. function fAge() {
  31. console.log(`依赖 age 属性的代码`);
  32. }
  33. // 不需要响应式的普通函数
  34. function bar() {
  35. console.log(`阿巴阿巴阿巴,不需要响应式的代码`)
  36. }
  37. // 响应式函数
  38. function watchFn(depend, fn) {
  39. depend.addDepend(fn)
  40. }
  41. // name age 变化
  42. obj.name = `ls`
  43. obj.age = 22
  44. // 现在要准备好哪个对象或属性改变后要执行的代码,只需要实例化 Depend 类就行
  45. // 给 obj 对象的 name 属性添加响应式操作
  46. const objNameDepend = new Depend()
  47. // 收集 name 属性的依赖
  48. watchFn(objNameDepend, fName1)
  49. watchFn(objNameDepend, fName2)
  50. // 执行(唤醒)name 属性的依赖
  51. objNameDepend.notify()
  52. // 给 obj 对象的 age 属性添加响应式操作
  53. const objAgeDepend = new Depend()
  54. // 收集 name 属性的依赖
  55. watchFn(objAgeDepend, fAge)
  56. // 执行(唤醒)name 属性的依赖
  57. objAgeDepend.notify()

依赖收集后对象的管理

现在我们已经通过一个依赖类可以收集好不同对象各自属性的依赖了。但又出现了一个问题,一个实例化对象代表一个属性的依赖,这么多对象这么多属性,势必造成大量的实例化对象。我们该怎么管理这些实例化对象呢?总不能全都堆在全局里吧。

我们需要一个容器来保存这些 depend 对象,Map 就很合适。map 的结构适合这种零散的数据,读取也快。
进一步,我们还可以用多个 Map 将这些对象组织起来建立一个树形结构,按对象和属于这个对象的属性建立联系,方便了后续响应式执行的时候可以精准找到要执行的依赖对象。
其中对象这一层级,可以选择 WeakMap,因为弱引用可以很快的回收对象。属性层级可以就使用 Map。(具体为什么还得再分析)

image.png

  1. // 收集依赖的类
  2. class Depend {
  3. constructor() {
  4. this.reactiveFns = [] // 每个实例化对象都维护了一个数组
  5. }
  6. // 将依赖放入数组中
  7. addDepend(fn) {
  8. this.reactiveFns.push(fn)
  9. }
  10. // 还可以把响应式执行封装一下
  11. notify() {
  12. this.reactiveFns.forEach(fn => {
  13. fn()
  14. })
  15. }
  16. }
  17. const obj = {
  18. name: 'zs',
  19. age: 18
  20. }
  21. const info = {
  22. name: 'w5',
  23. gender: 'man'
  24. }
  25. // obj 对象属性依赖
  26. function objName1() {
  27. // name 变化后,需要重新执行的代码
  28. console.log(obj.name)
  29. console.log(`与 name 属性相关的其他代码1`)
  30. }
  31. function objName2() {
  32. console.log(obj.name + 111)
  33. console.log(`与 name 属性相关的其他代码2`)
  34. }
  35. function objAge() {
  36. console.log(obj.age)
  37. console.log(`依赖 age 属性的代码`)
  38. }
  39. // info 对象的属性依赖
  40. function infoName() {
  41. console.log('info 对象,name 属性相关的代码')
  42. }
  43. function infoGender() {
  44. console.log('info 对象,gender 属性相关的代码')
  45. }
  46. // 不需要响应式的普通函数
  47. function bar() {
  48. console.log(`阿巴阿巴阿巴,不需要响应式的代码`)
  49. }
  50. // 响应式函数
  51. function watchFn(depend, fn) {
  52. depend.addDepend(fn)
  53. }
  54. // 收集依赖
  55. const objNameDep = new Depend()
  56. watchFn(objNameDep, objName1)
  57. watchFn(objNameDep, objName2)
  58. const objAgeDep = new Depend()
  59. watchFn(objAgeDep, objAge)
  60. const infoNameDep = new Depend()
  61. watchFn(infoNameDep, infoName)
  62. const infoGenderDep = new Depend()
  63. infoGenderDep.addDepend(infoGender)
  64. watchFn(infoGenderDep, infoGender)
  65. // 收集依赖的树形结构
  66. const objMap = new Map()
  67. objMap.set('objNameDep', objNameDep)
  68. objMap.set('objAgeDep', objAgeDep)
  69. const infoMap = new Map()
  70. infoMap.set('infoNameDep', infoNameDep)
  71. infoMap.set('infoGenderDep', infoGenderDep)
  72. const weakMap = new WeakMap()
  73. weakMap.set(obj, objMap)
  74. weakMap.set(info, infoMap)
  75. // obj.name 发生变化
  76. obj.name = `ls`
  77. // info.genter 发生变化
  78. info.gender = 'woman'
  79. // 响应式操作,
  80. // 1. 获取收集的依赖
  81. const objNameDepend = weakMap.get(obj).get('objNameDep')
  82. // 2. 执行响应式操作
  83. objNameDepend.notify()
  84. weakMap.get(info).get('infoGenderDep').notify()

现在发现手动将依赖存储到 map 中并将 map 放入 weakMap 维护这个树形结构是非常繁琐的。而且如果有些属性不会发生变化,那也不需要响应式操作了,那创建这个属性的依赖对象完全没必要。所以我们还是需要优化一下,需要响应式的创建 map 和 depend 对象,并且自动维护树形结构。

我们可以写一个 getDepend 函数专门来管理这种依赖关系:只要知道变化的对象和属性 ,就能找到 map 再找到 依赖对象,然后自动返回对应的 depend 对象。

getDepend 函数的实现需要响应的 对象和属性,所以我们先来完成响应式的实现,就是实现监听到对象的变化。

监听对象的变化

之前学过两种方式可以捕获对象的一些操作,这可以达到监听对象变化的效果。在收集依赖的基础上加上监听,完成响应式。更确切的说,需要监听的是 set 操作。

  • 方式一:通过 Object.defineProperty的方式(vue2采用的方式);
  • 方式二:通过new Proxy的方式(vue3采用的方式); ```javascript // 收集依赖的类 class Depend { constructor() { this.reactiveFns = [] // 每个实例化对象都维护了一个数组 } // 将依赖放入数组中 addDepend(fn) { this.reactiveFns.push(fn) } // 还可以把响应式执行封装一下 notify() { this.reactiveFns.forEach( fn => fn() ) } }

// 响应式函数 function watchFn(depend, fn) { depend.addDepend(fn) }

const obj = { name: ‘zs’, age: 18 }

// 依赖管理 const weakMap = new WeakMap() function getDepend(target, key) { // 具体实现略 return new Depend() }

// 响应式,捕获对象变化 const objProxy = new Proxy(obj, { set(target, key, newValue, receviecer) { Reflect.set(target, key, newValue, receviecer)

  1. // 获取obj对象依赖并执行
  2. const objDep = getDepend(target, key)
  3. objDep.notify()

}, get(target, key, receviecer) { return Reflect.get(target, key, receviecer) } })

// obj 对象属性依赖 function objName1() { // name 变化后,需要重新执行的代码 console.log(objProxy.name) console.log(与 name 属性相关的其他代码1) }

// 不需要响应式的普通函数 function bar() { console.log(阿巴阿巴阿巴,不需要响应式的代码) }

// obj.name 发生变化 objProxy.name = ls

  1. <a name="umKWu"></a>
  2. ## 依赖对象管理 `getDepend()`的实现
  3. 通过对象和属性,就查找weakmap 和 map 返回对应的依赖对象。
  4. ```javascript
  5. // 依赖管理
  6. const targetWeakMap = new WeakMap()
  7. function getDepend(target, key) {
  8. // 树形结构第一层,根据target对象获取map的过程
  9. let targetMap = targetWeakMap.get(target) // 第一次执行,weakMap 里啥也没有,所以获取的 targetMap 为空
  10. // 对象第一次变化的时候需要创建对象对应的 map,并将 map 放入 weakMap
  11. if (!targetMap) {
  12. // 但是第二次变化,就不需要重新创建 map 并放入 weakMap 了,所以可以加个非空判断
  13. targetMap = new Map()
  14. targetWeakMap.set(target, targetMap)
  15. }
  16. // 树形结构第二层,属性的依赖对象,根据key获取depend对象
  17. let keyDepend = targetMap.get(key)
  18. if (!keyDepend) {
  19. keyDepend = new Depend()
  20. targetMap.set(key, keyDepend)
  21. }
  22. return keyDepend
  23. }
  24. // 响应式,捕获对象变化
  25. const objProxy = new Proxy(obj, {
  26. set(target, key, newValue, receviecer) {
  27. Reflect.set(target, key, newValue, receviecer)
  28. // 获取obj对象依赖并执行
  29. const objDep = getDepend(target, key) // 因为还没收集依赖,所以这个 objDep 对象是空的
  30. objDep.notify()
  31. },
  32. get(target, key, receviecer) {
  33. return Reflect.get(target, key, receviecer)
  34. }
  35. })

正确的收集依赖

依赖管理完成了,那怎么收集依赖呢?换句话怎么调用 addDepend()方法,将依赖放入 依赖对象维护的数组中。
要想调用 addDepend 方法,首先得找到对应的 depend 对象。现在 depend 对象是通过 对象和属性 创建的,那只要知道响应的对象和属性就能获取 depend 对象了。
而在这些与属性相关的代码中,如:objName1(),会读取属性的值objProxy.name这不是会被代理对象捕获到 get 吗,get 捕获器里面的 target 和 key ,就是当前响应时,依赖代码所依赖的 对象和属性,刚好就可以拿到对应的依赖对象。
所以只需要让 objName1 函数执行一次就行,我们可以在 watchFn 函数中完成这一步,watchFn 焕发新春,又有用了。

  1. // obj 对象属性依赖
  2. function objName1() {
  3. // name 变化后,需要重新执行的代码
  4. console.log(objProxy.name)
  5. console.log(`与 name 属性相关的其他代码1`)
  6. }
  7. // 响应式函数
  8. // function watchFn(depend, fn) {
  9. // depend.addDepend(fn)
  10. // }
  11. function watchFn(fn) {
  12. fn() // 让依赖函数执行一次
  13. }
  14. watchFn(objName1)
  15. // 响应式,捕获对象变化
  16. const objProxy = new Proxy(obj, {
  17. set(target, key, newValue, receviecer) {
  18. Reflect.set(target, key, newValue, receviecer)
  19. // 获取obj对并执行
  20. const objDep = getDepend(target, key)
  21. objDep.notify()
  22. },
  23. get(target, key, receviecer) {
  24. // 收集依赖
  25. const keyDepend = getDepend(target, key) // 获取了对应的 depend 对象
  26. keyDepend.addDepend(fn)
  27. return Reflect.get(target, key, receviecer)
  28. }
  29. })

现在获取了响应的依赖对象,那怎么知道哪个函数和这个响应出来的依赖对象对应,需要被放入这个依赖对象的数组中呢?也就是说响应的依赖对象要收集哪些函数?
为了获取响应的依赖对象,让依赖的函数执行了一次,这个函数不就是要被收集的目标之一吗?所以可以维护一个全局变量,保存这个执行的函数,等函数执行完,获取对象后,再把它放入数组中。

  1. // 全局临时保存依赖函数的变量
  2. let activeReactiveFn = null
  3. function watchFn(fn) {
  4. activeReactiveFn = fn // 保存依赖函数
  5. fn() // 让依赖函数执行一次
  6. activeReactiveFn = null // 复原
  7. }
  8. // 响应式,捕获对象变化
  9. const objProxy = new Proxy(obj, {
  10. set(target, key, newValue, receviecer) {
  11. Reflect.set(target, key, newValue, receviecer)
  12. // 获取obj对并执行
  13. const objDep = getDepend(target, key)
  14. objDep.notify()
  15. },
  16. get(target, key, receviecer) {
  17. // 收集依赖
  18. const keyDepend = getDepend(target, key) // 获取了对应的 depend 对象
  19. keyDepend.addDepend(activeReactiveFn) // 收集对应的依赖
  20. return Reflect.get(target, key, receviecer)
  21. }
  22. })

现在只要函数被 wacthFn 包裹,就能被正确对应地收集了。

重构 Depend 类

结合上面的代码,已经可以基本实现响应式了。但是还有一个问题,就是当响应函数中多次使用了对象变量时,它会重复将这个函数添加到依赖数组中。

  1. // 收集依赖的类
  2. class Depend {
  3. constructor() {
  4. this.reactiveFns = [] // 每个实例化对象都维护了一个数组
  5. }
  6. // 将依赖放入数组中
  7. addDepend(fn) {
  8. this.reactiveFns.push(fn)
  9. }
  10. // 还可以把响应式执行封装一下
  11. notify() {
  12. this.reactiveFns.forEach(fn => {
  13. fn()
  14. })
  15. }
  16. }
  17. // 响应式函数
  18. let activeReactiveFn = null
  19. function watchFn(fn) {
  20. activeReactiveFn = fn // 保存依赖函数
  21. fn() // 让依赖函数执行一次
  22. activeReactiveFn = null // 复原
  23. }
  24. const obj = {
  25. name: 'zs',
  26. age: 18
  27. }
  28. // 依赖管理
  29. const targetWeakMap = new WeakMap()
  30. function getDepend(target, key) {
  31. // 树形结构第一层,根据target对象获取map的过程
  32. let targetMap = targetWeakMap.get(target)
  33. if (!targetMap) {
  34. targetMap = new Map()
  35. targetWeakMap.set(target, targetMap)
  36. }
  37. // 树形结构第二层,属性的依赖对象,根据key获取depend对象
  38. let keyDepend = targetMap.get(key)
  39. if (!keyDepend) {
  40. keyDepend = new Depend()
  41. targetMap.set(key, keyDepend)
  42. }
  43. return keyDepend
  44. }
  45. // 响应式,捕获对象变化
  46. const objProxy = new Proxy(obj, {
  47. set(target, key, newValue, receviecer) {
  48. Reflect.set(target, key, newValue, receviecer)
  49. // 获取obj对并执行
  50. const objDep = getDepend(target, key)
  51. objDep.notify()
  52. },
  53. get(target, key, receviecer) {
  54. // 收集依赖
  55. const keyDepend = getDepend(target, key) // 获取了对应的 depend 对象
  56. keyDepend.addDepend(activeReactiveFn) // 收集对应的依赖
  57. return Reflect.get(target, key, receviecer)
  58. }
  59. })
  60. // obj 对象属性依赖
  61. watchFn(() => {
  62. console.log(objProxy.name + 1111111111111)
  63. console.log(objProxy.name + 2222222222222)
  64. })
  65. // obj.name 发生变化
  66. objProxy.name = `ls`
  67. // zs1111111111111 // 在 watch 中调用了一次
  68. // zs2222222222222
  69. // ls1111111111111 // name 只发生了一次变化,可是依赖函数响应式地执行了两次
  70. // ls2222222222222
  71. // ls1111111111111
  72. // ls2222222222222

这是因为在watchFn 中执行的一次,执行了两个 objProxy.name,这就触发了代理对象两次 get 捕获,捕获一次就会将当前响应函数放入数组中,这样就相当于重复将响应函数添加到数组中了。

这个时候就希望存储依赖函数的容器可以自动去重,数组不行,但是 Set 却可以。所以需将数组改成 Set。

还有一个优化点。
捕获器 get 中添加响应函数到 Set 中,需要关注全局变量 activeReactiveFn。所以可以改写 addDepend 方法,将它改成无参数的方法,添加响应函数进容器的时候,直接添加 activeReactiveFn,反正 activeReactiveFn 是全局变量。

  1. // 保存当前需要收集的响应式函数
  2. let activeReactiveFn = null
  3. // 收集依赖的类
  4. class Depend {
  5. constructor() {
  6. this.reactiveFns = new Set() // 每个实例化对象维护了一个 Set
  7. }
  8. // 将依赖放入数组中
  9. addDepend() {
  10. // 非空,调用时说明当前实例化对象下的 activeReactiveFn 保存了对应的响应函数
  11. if(activeReactiveFn) {
  12. this.reactiveFns.add(activeReactiveFn) // 直接添加 activeReactiveFn
  13. }
  14. }
  15. // 还可以把响应式执行封装一下
  16. notify() {
  17. this.reactiveFns.forEach(fn => {
  18. fn()
  19. })
  20. }
  21. }

创建响应式对象

我们目前的响应式是针对于obj 一个对象的,我们可以用函数再封装一下代理对象的捕获过程

  1. // 给对象添加响应式的函数
  2. function reactive(obj) {
  3. // 返回代理对象
  4. return new Proxy(obj, {
  5. set(target, key, newValue, receviecer) {
  6. Reflect.set(target, key, newValue, receviecer)
  7. // 获取obj对并执行
  8. const objDep = getDepend(target, key)
  9. objDep.notify()
  10. },
  11. get(target, key, receviecer) {
  12. // 收集依赖
  13. const keyDepend = getDepend(target, key) // 获取了对应的 depend 对象
  14. keyDepend.addDepend() // 收集对应的依赖
  15. return Reflect.get(target, key, receviecer)
  16. }
  17. })
  18. }

vue3 的响应式原理

  1. // 保存当前需要收集的响应式函数
  2. let activeReactiveFn = null
  3. // 收集依赖的类
  4. class Depend {
  5. constructor() {
  6. this.reactiveFns = new Set() // 每个实例化对象维护了一个 Set
  7. }
  8. // 将依赖放入数组中
  9. addDepend() {
  10. // 非空,调用时说明当前实例化对象下的 activeReactiveFn 保存了对应的响应函数
  11. if (activeReactiveFn) {
  12. this.reactiveFns.add(activeReactiveFn) // 直接添加 activeReactiveFn
  13. }
  14. }
  15. // 还可以把响应式执行封装一下
  16. notify() {
  17. this.reactiveFns.forEach(fn => {
  18. fn()
  19. })
  20. }
  21. }
  22. // 响应式函数
  23. function watchFn(fn) {
  24. activeReactiveFn = fn // 保存依赖函数
  25. fn() // 让依赖函数执行一次
  26. activeReactiveFn = null // 复原
  27. }
  28. // 依赖对象管理,获取依赖对象
  29. const targetWeakMap = new WeakMap()
  30. function getDepend(target, key) {
  31. // 树形结构第一层,根据target对象获取map的过程
  32. let targetMap = targetWeakMap.get(target)
  33. if (!targetMap) {
  34. targetMap = new Map()
  35. targetWeakMap.set(target, targetMap)
  36. }
  37. // 树形结构第二层,属性的依赖对象,根据key获取depend对象
  38. let keyDepend = targetMap.get(key)
  39. if (!keyDepend) {
  40. keyDepend = new Depend()
  41. targetMap.set(key, keyDepend)
  42. }
  43. return keyDepend
  44. }
  45. // 给对象添加响应式的函数
  46. function reactive(obj) {
  47. // 响应式,捕获对象变化,直接返回代理对象
  48. return new Proxy(obj, {
  49. set(target, key, newValue, receviecer) {
  50. Reflect.set(target, key, newValue, receviecer)
  51. // 获取obj对并执行
  52. const objDep = getDepend(target, key)
  53. objDep.notify()
  54. },
  55. get(target, key, receviecer) {
  56. // 收集依赖
  57. const keyDepend = getDepend(target, key) // 获取了对应的 depend 对象
  58. keyDepend.addDepend() // 收集对应的依赖
  59. return Reflect.get(target, key, receviecer)
  60. }
  61. })
  62. }
  63. const obj = {
  64. name: 'zs',
  65. age: 18
  66. }
  67. // 给 obj 对象增加响应式处理
  68. const objProxy = reactive(obj)
  69. // obj 对象属性依赖
  70. watchFn(() => {
  71. console.log(objProxy.name + 1111111111111)
  72. console.log(objProxy.name + 2222222222222)
  73. })
  74. objProxy.name = 'ls'
  75. // 给 info 对象增加响应式处理
  76. // 直接在声明的时候就添加,表明看是 info ,其实是 info 的代理对象
  77. const info = reactive({
  78. name: 'w5',
  79. gender: 'man'
  80. })
  81. // 自动执行的依赖函数
  82. watchFn(() => {
  83. console.log(info.gender);
  84. console.log('gender 属性变化了');
  85. })
  86. info.gender = 'woman'
  87. // zs1111111111111
  88. // zs2222222222222
  89. // ls1111111111111
  90. // ls2222222222222
  91. // man
  92. // gender 属性变化了
  93. // woman
  94. // gender 属性变化了

vue2 的响应式原理

vue2 的响应式其他的逻辑都是一样的,无非就是监听对象变化用的是 Object.defineProperty()中的 setter、getter。

  1. function reactive(obj) {
  2. // 遍历属性,监听每个属性
  3. Object.keys(obj).forEach(key => {
  4. // 保存当前属性值
  5. let value = obj[key]
  6. // set,get 设置响应式逻辑
  7. Object.defineProperty(obj, key, {
  8. get() {
  9. // 收集依赖
  10. getDepend(obj, key).addDepend()
  11. return value
  12. },
  13. set(newValue) {
  14. value = newValue
  15. // 获取依赖对象,唤醒依赖
  16. getDepend(obj, key).notify()
  17. }
  18. })
  19. })
  20. // 返回处理过属性描述符的对象
  21. return obj
  22. }
  1. // 保存当前需要收集的响应式函数
  2. let activeReactiveFn = null
  3. // 收集依赖的类
  4. class Depend {
  5. constructor() {
  6. this.reactiveFns = new Set() // 每个实例化对象维护了一个 Set
  7. }
  8. // 将依赖放入数组中
  9. addDepend() {
  10. // 非空,调用时说明当前实例化对象下的 activeReactiveFn 保存了对应的响应函数
  11. if (activeReactiveFn) {
  12. this.reactiveFns.add(activeReactiveFn) // 直接添加 activeReactiveFn
  13. }
  14. }
  15. // 还可以把响应式执行封装一下
  16. notify() {
  17. this.reactiveFns.forEach(fn => {
  18. fn()
  19. })
  20. }
  21. }
  22. // 响应式函数
  23. function watchFn(fn) {
  24. activeReactiveFn = fn // 保存依赖函数
  25. fn() // 让依赖函数执行一次
  26. activeReactiveFn = null // 复原
  27. }
  28. // 依赖管理
  29. const targetWeakMap = new WeakMap()
  30. function getDepend(target, key) {
  31. // 树形结构第一层,根据target对象获取map的过程
  32. let targetMap = targetWeakMap.get(target)
  33. if (!targetMap) {
  34. targetMap = new Map()
  35. targetWeakMap.set(target, targetMap)
  36. }
  37. // 树形结构第二层,属性的依赖对象,根据key获取depend对象
  38. let keyDepend = targetMap.get(key)
  39. if (!keyDepend) {
  40. keyDepend = new Depend()
  41. targetMap.set(key, keyDepend)
  42. }
  43. return keyDepend
  44. }
  45. // 给对象添加响应式的函数
  46. function reactive(obj) {
  47. // 遍历属性,监听每个属性
  48. Object.keys(obj).forEach(key => {
  49. // 保存当前属性值
  50. let value = obj[key]
  51. // set,get 设置响应式逻辑
  52. Object.defineProperty(obj, key, {
  53. get() {
  54. // 收集依赖
  55. getDepend(obj, key).addDepend()
  56. return value
  57. },
  58. set(newValue) {
  59. value = newValue
  60. // 获取依赖对象,唤醒依赖
  61. getDepend(obj, key).notify()
  62. }
  63. })
  64. })
  65. // 返回处理过属性描述符的对象
  66. return obj
  67. }
  68. // 给 info 对象增加响应式处理
  69. // 直接在声明的时候就添加,表明看是 info ,其实是处理过属性描述符的 info 对象
  70. const info = reactive({
  71. name: 'w5',
  72. gender: 'man'
  73. })
  74. // 自动执行的依赖函数
  75. watchFn(() => {
  76. console.log(info.gender)
  77. console.log('gender 属性变化了')
  78. })
  79. info.gender = 'woman'
  80. // man
  81. // gender 属性变化了
  82. // woman
  83. // gender 属性变化了

响应式大总结

  1. 依赖类收集依赖
    1. 构造函数中维护一个 Set 集合
    2. 一个将依赖函数添加到集合的方法
    3. 一个执行集合冲依赖函数的方法
  2. 响应式对象函数
    1. 两种监听对象的方式
      1. Proxy + Reflect:handler 对象中捕获器捕获对象的属性操作
      2. Object.defineProperty():遍历 key,挨个设置属性描述符
    2. get 中返回属性值外,还获取依赖对象并收集依赖到 Set 集合中
    3. set 中除设置值外,还获取依赖对象并唤醒依赖函数执行
    4. 返回处理过后支持响应式对象
  3. 依赖对象管理,响应式获取依赖对象
    1. 根据目标对象从 WeakMap 中获取对应的 Map
    2. 再根据目标对象的属性从 Map 中获取对应的依赖对象
    3. 注意处理第一次的响应时新建依赖对象和 Map
  4. 响应式函数
    1. 将依赖函数存入响应式函数执行一次,并赋值给全局变量
    2. 全局变量注意复原
  5. 直接在 reactIve 函数中定义对象,之后就使用处理过的对象
  6. 对象属性操作直接在 watchFn 函数中定义,成为待收集的依赖