Vue 3响应式原理及实现

开发 前端
从零开始实现你自己的响应式库,从零开始实现 Vue 3 响应式模块。

 [[329472]]

从零开始实现你自己的响应式库,从零开始实现 Vue 3 响应式模块。 

本文完整内容见buid-your-own-vue-next

1. 实现响应式

响应基本类型变量

首先看一下响应式预期应该是什么样的,新建一个 demo.js 文件,内容如下: 

  1. // 这种写成一行完全是为了节省空间,实际上我会一行一个变量  
  2. let a = 1b = 2c = a * b  
  3. console.log('c:' + c) // 2  
  4. a = 2  
  5. console.log('c:' + c) // 期望得到4 

思考一下,如何才能做到当 a 变动时 c 跟着变化?

显然,我们需要做的就是重新执行一下 let c = a * b 即可,像这样: 

  1. let a = 1b = 2c = a * b  
  2. console.log('c:' + c) // 2  
  3. a = 2  
  4. c = a * b  
  5. console.log('c:' + c) // 期望得到4 

那么,现在我们把需要重新执行的代码写成一个函数,代码如下: 

  1. let a = 1b = 2c = 0  
  2. let effect = () => { c = a * b }  
  3. effect() // 首次执行更新c的值  
  4. console.log('c:' + c) // 2  
  5. a = 2  
  6. console.log('c:' + c) // 期望得到4 

现在仍然没有达成预期的效果,实际上我们还需要两个方法,一个用来存储所有需要依赖更新的 effect,我们假设叫 track,一个用来触发执行这些 effect 函数,假设叫做 trigger。

注意: 这里我们的函数命名和 Vue 3 中保持一致,从而可以更容易理解 Vue 3 源码。

代码类似这样: 

  1. let a = 1b = 2c = 0  
  2. let effect = () => { c = a * b }  
  3. track() // 收集 effect   
  4. effect() // 首次执行更新c的值  
  5. console.log('c:' + c) // 2  
  6. a = 2  
  7. trigger() // a变化时,触发effect的执行  
  8. console.log('c:' + c) // 期望得到4 

那么 track 和 trigger 分别做了什么,是如何实现的呢?我们暂且可以简单理解为一个“发布-订阅者模式”,track 就是不断给一个数组 dep 添加 effect,trigger 用来遍历执行 dep 的每一项 effect。

现在来完成这两个函数 

  1. let a = 1b = 2c = 0  
  2. let effect = () => { c = a * b }  
  3. let dep = new Set()  
  4. let track = () => { dep.add(effect) }  
  5. let trigger = () => { dep.forEach(effect => effect()) }  
  6. track()  
  7. effect() // 首次执行更新c的值  
  8. console.log('c:' + c) // 2  
  9. a = 2 
  10. trigger() // a变化时,触发effect的执行  
  11. console.log('c:' + c) // 期望得到4,实际得到4 

注意这里我们使用 Set 来定义 dep,原因就是 Set 本身不能添加重复的 key,读写都非常方便。

现在代码的执行结果已经符合预期了。 

  1. c: 2  
  2. c: 4 

响应对象的不同属性

通常情况,我们定义的对象都有很多的属性,每一个属性都需要有自己的 dep(即每个属性都需要把那些依赖了自己的effect记录下来放进自己的 new Set() 中),如何来实现这样的功能呢?

有一段代码如下: 

  1. let obj = { a: 10, b: 20 }  
  2. let timesA = obj.a * 10  
  3. let divideA = obj.a / 10  
  4. let timesB = obj.b * 10  
  5. let divideB = obj.b / 10  
  6. // 100, 1, 200, 2  
  7. console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`)  
  8. obj.a = 100  
  9. obj.b = 200  
  10. // 期望得到 1000, 10, 2000, 20  
  11. console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`) 

这段代码中,按照上文讲解的,属性a和b的dep应该是如下: 

  1. let depA = [  
  2.   () => { timesA = obj.a * 10 },  
  3.   () => { divideA = obj.a / 10 }  
  4.  
  5. let depB = [   
  6.   () => { timesB = obj.b * 10 },  
  7.   () => { divideB = obj.b / 10 }  

如果代码还是按照前文的方式来写显然是不科学的,这里就要开始做一点点抽象了,收集依赖我们可以假想用track('a') track('b')这种形式分别记录对象不同key的依赖项,那么显然我们还需要一个东西来存放这些 key 及相应的dep。

现在我们来实现这样的 track 函数及对应的 trigger 函数,代码如下: 

  1. const depsMap = new Map() // 每一项都是一个 Set 对象  
  2. function track(key) {  
  3.   let dep = depsMap.get(key)  
  4.   if(!dep) {  
  5.     depsMap.set(key, dep = new Set());  
  6.   } 
  7.    dep.add(effect)  
  8.  
  9. function trigger(key) {  
  10.   let dep = depsMap.get(key)  
  11.   if(dep) {  
  12.     dep.forEach(effect => effect())  
  13.   }  

这样就实现了对一个对象不同属性的依赖收集,那么现在这个代码最简单的使用方法将是下面这样: 

  1. const depsMap = new Map() // 每一项都是一个 Set 对象  
  2. function track(key) { 
  3.   ...  
  4.   // only for usage demo  
  5.   if(key === 'a'){  
  6.     dep.add(effectTimesA)  
  7.     dep.add(effectDivideA)  
  8.   }else if(key === 'b'){  
  9.     dep.add(effectTimesB)  
  10.     dep.add(effectDivideB)  
  11.   }  
  12.  
  13. function trigger(key) {  
  14.   ...  
  15.  
  16. let obj = { a: 10, b: 20 }  
  17. let timesA = 0  
  18. let divideA = 0  
  19. let timesB = 0  
  20. let divideB = 0  
  21. let effectTimesA = () => { timesA = obj.a * 10 }  
  22. let effectDivideA = () => { divideA = obj.a / 10 }  
  23. let effectTimesB = () => { timesB = obj.b * 10 }  
  24. let effectDivideB = () => { divideB = obj.b / 10 }  
  25. track('a')  
  26. track('b')  
  27. // 为了省事直接改成调用trigger,后文同样  
  28. trigger('a')  
  29. trigger('b')  
  30. // 100, 1, 200, 2  
  31. console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`)  
  32. obj.a = 100  
  33. obj.b = 200  
  34. trigger('a')  
  35. trigger('b')  
  36. // 期望得到:1000, 10, 2000, 20 实际得到:1000, 10, 2000, 20  
  37. console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`) 

代码看起来仍然是臃肿无比,别着急,后面的设计会优化这个问题。

响应多个对象

我们已经实现了对一个对象的响应编程,那么要对多个对象实现响应式编程该怎么做呢?

脑袋一拍,继续往外嵌套一层对象不就可以了吗?没错,你可以用 ES6 中的 WeakMap 轻松实现,WeakMap 刚好可以(只能)把对象当作 key。(题外话,Map 和 WeakMap 的区别

我们假想实现后是这样的效果: 

  1. let obj1 = { a: 10, b: 20 }  
  2. let obj2 = { c: 30, d: 40 }  
  3. const targetMap = new WeakMap()  
  4. // 省略代码  
  5. // 获取 obj1 的 depsMap  
  6. // 获取 obj2 的 depsMap  
  7. targetMap.set(obj1, "obj1's depsMap")  
  8. targetMap.set(obj2, "obj2's depsMap") 

这里暂且不纠结为什么叫 targetMap,现在整体依赖关系如下:

名称 类型 key
targetMap WeakMap object depsMap
depsMap Map property dep
dep Set   effect
  •  targetMap: 存放每个响应式对象(所有属性)的依赖项
  •  targetMap: 存放响应式对象每个属性对应的依赖项
  •  dep: 存放某个属性对应的所有依赖项(当这个对象对应属性的值发生变化时,这些依赖项函数会重新执行)

现在我们可以实现这个功能了,核心代码如下: 

  1. const targetMap = new WeakMap();  
  2. function track(target, key) {  
  3.   let depsMap = targetMap.get(target)  
  4.   if(!depsMap){  
  5.     targetMap.set(target, depsMap = new Map()) 
  6.   }  
  7.   let dep = depsMap.get(key)  
  8.   if(!dep) {  
  9.     depsMap.set(key, dep = new Set());  
  10.   }  
  11.   // 先忽略这个  
  12.   dep.add(effect)  
  13.  
  14. function trigger(target, key) {  
  15.   let depsMap = targetMap.get(target)  
  16.   if(depsMap){  
  17.     let dep = depsMap.get(key)  
  18.     if(dep) {  
  19.       dep.forEach(effect => effect())  
  20.     }  
  21.   }  

那么现在这个代码最简单的使用方法将是下面这样: 

  1. const targetMap = new WeakMap();  
  2. function track(target, key) {  
  3.   ...  
  4.   // only for usage demo  
  5.   if(key === 'a'){  
  6.     dep.add(effectTimesA)  
  7.     dep.add(effectDivideA)  
  8.   }  
  9. function trigger(target, key) {  
  10.   ...  
  11.  
  12. let obj = { a: 10, b: 20 }  
  13. let timesA = 0  
  14. let divideA = 0  
  15. let effectTimesA = () => { timesA = obj.a * 10 }  
  16. let effectDivideA = () => { divideA = obj.a / 10 }  
  17. track(obj, 'a')  
  18. trigger(obj, 'a')  
  19. console.log(`${timesA}, ${divideA}`) // 100, 1  
  20. obj.a = 100  
  21. trigger(obj, 'a')  
  22. console.log(`${timesA}, ${divideA}`) // 1000, 10 

至此,我们对响应式的基本概念有了了解,我们已经做到了收集所有响应式对象的依赖项,但是现在你可以看到代码的使用是极其繁琐的,主要是因为我们还没实现自动收集依赖项、自动触发修改。

2. Proxy 和 Reflect

上一节讲到了我们实现了基本的响应功能,但是我们目前还是手动进行依赖收集和触发更新的。

解决这个问题的方法应该是:

  •  当访问(GET)一个属性时,我们就调用 track(obj, <property>) 自动收集依赖项(存储 effect)
  •  当修改(SET)一个属性时,我们就调用 trigger(obj, <property> 自动触发更新(执行存储的effect)

那么现在问题就是,我们如何在访问或修改一个属性时做到这样的事情?也即是如何拦截这种 GET 和 SET 操作?

Vue 2中我们使用 ES5 中的 Object.defineProperty 来拦截 GET 和 SET。

Vue 3中我们将使用 ES6 中的 Reflect 和 Proxy。(注意:Vue 3不再支持IE浏览器,所以可以用比较多的高级特性)

我们先来看一下怎么输出一个对象的一个属性值,可以用下面这三种方法:

  •  使用 . => obj.a
  •  使用 [] => obj['a']
  •  使用 ES6 中的 Reflect => Reflect.get(obj, 'a')

这三种方法都是可行的,但是 Reflect 有非常强大的能力,后面会讲到。

Proxy

我们先来看看 Proxy,Proxy 是另一个对象的占位符,默认是对这个对象的委托。你可以在这里查看 Proxy 更详细的用法。 

  1. let obj = { a: 1}  
  2. let proxiedObj = new Proxy(obj, {})  
  3. console.log(proxiedObj.a) // 1 

这个过程可以表述为,获取 proxiedObj.a 时,直接去从查找 obj.a然后返回给 proxiedObj,再输出 proxiedObj.a。

Proxy 的第二个参数被称为 handler,handler就是包含捕捉器(trap)的占位符对象,即处理器对象,捕捉器允许我们拦截一些基本的操作,如:

  •  查找属性
  •  枚举
  •  函数的调用

现在我们的示例代码修改为: 

  1. let obj = { a: 1}  
  2. let proxiedObj = new Proxy(obj, {  
  3.   get(target, key) {  
  4.     console.log('Get')  
  5.     return target[key]  
  6.   }  
  7. })  
  8. console.log(proxiedObj.a) // 1 

这段代码中,我们直接使用 target[key] 返回值,它直接返回了原始对象的值,不做任何其它操作,这对于这个简单的示例来说没任何问题,。

现在我们看一下下面这段稍微复杂一点的代码: 

  1. let obj = {  
  2.   a: 1,  
  3.   get b() { return this.a }  
  4. let proxiedObj = new Proxy(obj, {  
  5.   get(target, key, receiver) {  
  6.     return target[key] // 这里的target是obj  
  7.   }  
  8. })  
  9. let childObj = Object.create(proxiedObj)  
  10. childObj.a = 2  
  11. console.log(childObj.b) // 期望得到2 实际输出1 

这段代码的输出结果就是错误的,这是什么情况?难道是原型继承写错了吗?我们尝试把Proxy相关代码去掉,发现输出是正常的......

这个问题其实就出在 return target[key]这一行:

  1.  当读取 childObj.b 时,childObj 上没有属性 b,因此会从原型链上查找
  2.  原型链是 proxiedObj
  3.  读取 proxiedObj.b 时,会触发Proxy捕捉器(trap)中的 get,这直接从原始对象中返回了 target[key]
  4.  这里target[key] 中 key 是一个 getter,因此这个 getter 中的上下文 this 即为target,这里的 target 就是 obj,因此直接返回了 1。

参考 为什么要使用 Reflect 

那么我们怎么解决这个 this 出错的问题呢?

Reflect

现在我们就可以讲讲 Reflect 了。你可以在这里查看 Reflect 更详细的用法。

捕获器 get 有第三个参数叫做 receiver。

Proxy 中 handler.get(target, prop, receiver) 中的参数 receiver :Proxy 或者继承 Proxy 的对象。

Reflect.get(target, prop, receiver) 中的参数 receiver :如果target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。

这确保了当我们的对象从另一个对象继承了值或函数时使用 this 值的正确性。

我们修改刚才的示例如下: 

  1. let obj = {  
  2.   a: 1,  
  3.   get b() { return this.a }  
  4.  
  5. let proxiedObj = new Proxy(obj, {  
  6.   // 本例中这里的receiver为调用时的对象childOjb  
  7.   get(target, key, receiver) {  
  8.     // 这里的target是obj  
  9.     // 这意思是把receiver作为this去调用target[key]  
  10.     return Reflect.get(target, key, receiver)  
  11.   }  
  12. })  
  13. let childObj = Object.create(proxiedObj)  
  14. childObj.a = 2 
  15. console.log(childObj.b) // 期望得到2 实际输出1 

现在我们弄清楚了为什么要结合 Reflect 来使用 Proxy,有了这些知识,就可以继续完善我们的代码了。

实现reactive函数

现在修改我们的示例代码为: 

  1. let obj = { a: 1}  
  2. let proxiedObj = new Proxy(obj, {  
  3.   get(target, key, receiver) {  
  4.     console.log('Get')  
  5.     return Reflect.get(target, key, receiver)  
  6.   }  
  7.   set(target, key, value, receiver) {  
  8.     console.log('Set')  
  9.     return Reflect.set(target, key, value, receiver)  
  10.   }  
  11. }) 
  12. console.log(proxiedObj.a) // Get 1 

接下来我们要做的就是结合 Proxy 的 handler 和 之前实现了的 track、trigger 来完成一个响应式模块。

首先,我们来封装一下 Proxy 相关代码,和Vue 3保持一致叫reactive。 

  1. function reactive(target) {  
  2.   const handler = {  
  3.     get(target, key, receiver) { 
  4.       return Reflect.get(target, key, receiver)  
  5.     },  
  6.     set(target, key, value, receiver) {  
  7.       return Reflect.set(target, key, value, receiver)  
  8.     }  
  9.   }  
  10.   return new Proxy(target, handler)  

这里有一个问题,当我们每次调用 reactive 时都会重新定义一个 handler 的对象,为了优化这个,我们把 handler 提出去,代码如下: 

  1. const reactiveHandler = {  
  2.   get(target, key, receiver) {  
  3.     return Reflect.get(target, key, receiver)  
  4.   },  
  5.   set(target, key, value, receiver) {  
  6.     return Reflect.set(target, key, value, receiver)  
  7.   }  
  8.  
  9. function reactive(target) {  
  10.   return new Proxy(target, reactiveHandler)  

现在把reactive引入到我们的第一节中最后的示例代码中。 

  1. let obj = reactive({ a: 10, b: 20 })  
  2. let timesA = 0  
  3. let divideA = 0  
  4. let effectTimesA = () => { timesA = obj.a * 10 }  
  5. let effectDivideA = () => { divideA = obj.a / 10 }  
  6. track(obj, 'a')  
  7. trigger(obj, 'a')  
  8. console.log(`${timesA}, ${divideA}`) // 100, 1  
  9. obj.a = 100  
  10. trigger(obj, 'a')  
  11. console.log(`${timesA}, ${divideA}`) // 1000, 10 

现在我们要做的是去掉示例代码中的 track 和 trigger。

回到本节开头提出的解决方案,我们已经可以拦截 GET 和 SET 操作了,只需要在适当的时候调用 track 和 trigger 方法即可,我们修改 reactiveHandler 代码如下: 

  1. const reactiveHandler = {  
  2.   get(target, key, receiver) {  
  3.     const result = Reflect.get(target, key, receiver)  
  4.     track(target, key)  
  5.     return result  
  6.   },  
  7.   set(target, key, value, receiver) {  
  8.     const oldVal = target[key]  
  9.     const result = Reflect.set(target, key, value, receiver)  
  10.     // 这里判断条件不对,result为一个布尔值  
  11.     if(oldVal !== result){ 
  12.        trigger(target, key)  
  13.     }  
  14.     return result  
  15.   }  

现在我们的示例代码可以精简为这样: 

  1. let obj = reactive({ a: 10, b: 20 })  
  2. let timesA = 0  
  3. let divideA = 0  
  4. let effectTimesA = () => { timesA = obj.a * 10 }  
  5. let effectDivideA = () => { divideA = obj.a / 10 }  
  6. // 恢复调用 effect 的形式  
  7. effectTimesA()  
  8. effectDivideA()  
  9. console.log(`${timesA}, ${divideA}`) // 100, 1  
  10. obj.a = 100  
  11. console.log(`${timesA}, ${divideA}`) // 1000, 10 

我们已经去掉了手动 track 和 trigger 代码,至此,我们已经实现了 reactive 函数,看起来和Vue 3源码差不多了。

但这还有点问题:

  •  track 函数中的 effect 现在还没处理,只能手动添加
  •  reactive 现在只能作用于对象,基本类型变量怎么处理?

下一个章节我们将解决这个问题,让我们的代码更加接近Vue 3。

3. activeEffect 和 ref

首先,我们修改一下示例代码: 

  1. let obj = reactive({ a: 10, b: 20 })  
  2. let timesA = 0  
  3. let effect = () => { timesA = obj.a * 10 }  
  4. effect()  
  5. console.log(timesA) // 100  
  6. obj.a = 100  
  7. // 新增一行,使用到obj.a  
  8. console.log(obj.a)  
  9. console.log(timesA) // 1000 

由上节知识可以知道,当 effect 执行时我们访问到了 obj.a,因此会触发 track 收集该依赖 effect。同理,console.log(obj.a) 这一行也同样触发了 track,但这并不是响应式代码,我们预期不触发 track。

我们想要的是只在 effect 中的代码才触发 track。

能想到怎么来实现吗?

只响应需要依赖更新的代码(effect)

首先,我们定义一个变量 shouldTrack,暂且认为它表示是否需要执行 track,我们修改 track 代码,只需要增加一层判断条件,如下: 

  1. const targetMap = new WeakMap();  
  2. let shouldTrack = null  
  3. function track(target, key) {  
  4.   if(shouldTrack){  
  5.     let depsMap = targetMap.get(target)  
  6.     if(!depsMap){  
  7.       targetMap.set(target, depsMap = new Map())  
  8.     }  
  9.     let dep = depsMap.get(key)  
  10.     if(!dep) {  
  11.       depsMap.set(key, dep = new Set());  
  12.     }  
  13.     // 这里的 effect 为使用时定义的 effect  
  14.     // shouldTrack 时应该把对应的 effect 传进来  
  15.     dep.add(effect)  
  16.     // 如果有多个就手写多个  
  17.     // dep.add(effect1)  
  18.     // ...  
  19.   }  

现在我们需要解决的就是 shouldTrack 赋值问题,当有需要响应式变动的地方,我们就写一个 effect 并赋值给 shouldTrack,然后 effect 执行完后重置 shouldTrack 为 null,这样结合刚才修改的 track 函数就解决了这个问题,思路如下: 

  1. let shouldTrack = null  
  2. // 这里省略 track trigger reactive 代码  
  3. ...  
  4. let obj = reactive({ a: 10, b: 20 })  
  5. let timesA = 0 
  6. let effect = () => { timesA = obj.a * 10 }  
  7. shouldTrack = effect // (*) 
  8. effect()  
  9. shouldTrack = null // (*)  
  10. console.log(timesA) // 100  
  11. obj.a = 100  
  12. console.log(obj.a)  
  13. console.log(timesA) // 1000 

此时,执行到 console.log(obj.a) 时,由于 shouldTrack 值为 null,所以并不会执行 track,完美。

完美了吗?显然不是,当有很多的 effect 时,你的代码会变成下面这样: 

  1. let effect1 = () => { timesA = obj.a * 10 }  
  2. shouldTrack = effect1 // (*)  
  3. effect1()  
  4. shouldTrack = null // (*)  
  5. let effect2 = () => { timesB = obj.a * 10 }  
  6. shouldTrack = effect1 // (*)  
  7. effect2()  
  8. shouldTrack = null // (*) 

我们来优化一下这个问题,为了和Vue 3保持一致,这里我们修改 shouldTrack 为 activeEffect,现在它表示当前运行的 effect。

我们把这段重复使用的代码封装成函数,如下: 

  1. let activeEffect = null  
  2. // 这里省略 track trigger reactive 代码  
  3. ...  
  4. function effect(eff) {  
  5.   activeEffect = eff  
  6.   activeEffect()  
  7.   activeEffect = null  

同时我们还需要修改一下 track 函数: 

  1. function track(target, key) {  
  2.   if(activeEffect){  
  3.     ...  
  4.     // 这里不用再根据条件手动添加不同的 effect 了!  
  5.     dep.add(activeEffect)  
  6.   } 

那么现在的使用方法就变成了: 

  1. const targetMap = new WeakMap();  
  2. let activeEffect = null  
  3. function effect (eff) { ... }  
  4. function track() { ... }  
  5. function trigger() { ... }  
  6. function reactive() { ... }  
  7. let obj = reactive({ a: 10, b: 20 })  
  8. let timesA = 0  
  9. let timesB = 0  
  10. effect(() => { timesA = obj.a * 10 })  
  11. effect(() => { timesB = obj.b * 10 })  
  12. console.log(timesA) // 100  
  13. obj.a = 100  
  14. console.log(obj.a)  
  15. console.log(timesA) // 1000 

现阶段完整代码

现在新建一个文件reactive.ts,内容就是当前实现的完整响应式代码: 

  1. const targetMap = new WeakMap();  
  2. let activeEffect = null  
  3. function effect(eff) {  
  4.   activeEffect = eff  
  5.   activeEffect()  
  6.   activeEffect = null  
  7.  
  8. function track(target, key) {  
  9.   if(activeEffect){  
  10.     let depsMap = targetMap.get(target)  
  11.     if(!depsMap){  
  12.       targetMap.set(target, depsMap = new Map())  
  13.     }  
  14.     let dep = depsMap.get(key)  
  15.     if(!dep) {  
  16.       depsMap.set(key, dep = new Set());  
  17.     }  
  18.     dep.add(activeEffect)  
  19.   }  
  20.  
  21. function trigger(target, key) {  
  22.   let depsMap = targetMap.get(target)  
  23.   if(depsMap){  
  24.     let dep = depsMap.get(key)  
  25.     if(dep) {  
  26.       dep.forEach(effect => effect())  
  27.     }  
  28.   }  
  29.  
  30. const reactiveHandler = {  
  31.   get(target, key, receiver) {  
  32.     const result = Reflect.get(target, key, receiver)  
  33.     track(target, key)  
  34.     return result  
  35.   }, 
  36.    set(target, key, value, receiver) {  
  37.     const oldVal = target[key]  
  38.     const result = Reflect.set(target, key, value, receiver)  
  39.     if(oldVal !== result){  
  40.       trigger(target, key)  
  41.     }  
  42.     return result  
  43.   }  
  44.  
  45. function reactive(target) {  
  46.   return new Proxy(target, reactiveHandler)  

现在我们已经解决了非响应式代码也触发track的问题,同时也解决了上节中留下的问题:track 函数中的 effect 只能手动添加。

接下来我们解决上节中留下的另一个问题:reactive 现在只能作用于对象,基本类型变量怎么处理?

实现ref

修改 demo.js 代码如下: 

  1. import { effect, reactive } from "./reactive"  
  2. let obj = reactive({ a: 10, b: 20 })  
  3. let timesA = 0  
  4. let sum = 0  
  5. effect(() => { timesA = obj.a * 10 })  
  6. effect(() => { sum = timesA + obj.b })    
  7. obj.a = 100  
  8. console.log(sum) // 期望: 1020 

这段代码并不能实现预期效果,因为当 timesA 正常更新时,我们希望能更新 sum(即重新执行 () => { sum = timesA + obj.b }),而实际上由于 timesA 并不是一个响应式对象,没有 track 其依赖,所以这一行代码并不会执行。

那我们如何才能让这段代码正常工作呢?其实我们把基本类型变量包装成一个对象去调用 reactive 即可。

看过 Vue composition API 的同学可能知道,Vue 3中用一个 ref 函数来实现把基本类型变量变成响应式对象,通过 .value 获取值,ref 返回的就是一个 reactive 对象。

实现这样的一个有 value 属性的对象有这两种方法:

  1.  直接给一个对象添加 value 属性 
  1. function ref(intialValue) {  
  2.   return reactive({  
  3.     value: intialValue  
  4.   })  
  1.  用 getter 和 setter 来实现 
  1. function ref(raw) {  
  2.   const r = {  
  3.     get value() {  
  4.       track(r, 'value')  
  5.       return raw  
  6.     },  
  7.     set value(newVal) {  
  8.       raw = newVal  
  9.       trigger(r, 'value)  
  10.     }  
  11.   }  
  12.   return r  

现在我们的示例代码修改成: 

  1. import { effect, reactive } from "./reactive"  
  2. function ref(intialValue) {  
  3.   return reactive({  
  4.     value: intialValue  
  5.   })  
  6.  
  7. let obj = reactive({ a: 10, b: 20 })  
  8. let timesA = ref(0)  
  9. let sum = 0  
  10. effect(() => { timesA.value = obj.a * 10 })  
  11. effect(() => { sum = timesA.value + obj.b })  
  12. // 期望: timesA: 100  sum: 120 实际:timesA: 100  sum: 120  
  13. console.log(`timesA: ${timesA.value}  sum: ${sum}`)  
  14. obj.a = 100  
  15. // 期望: timesA: 1000  sum: 1020 实际:timesA: 1000  sum: 1020  
  16. console.log(`timesA: ${timesA}  sum: ${sum}`) 

增加了 ref 处理基本类型变量后,我们的示例代码运行结果符合预期了。至此我们已经解决了遗留问题:reactive 只能作用于对象,基本类型变量怎么处理?

Vue 3中的 ref 是用第二种方法来实现的,现在我们整理一下代码,把 ref 放到 reactive.j 中。

现阶段完整代码 

  1. const targetMap = new WeakMap(); 
  2. let activeEffect = null  
  3. function effect(eff) {  
  4.   activeEffect = eff  
  5.   activeEffect()  
  6.   activeEffect = null  
  7.  
  8. function track(target, key) {  
  9.   if(activeEffect){  
  10.     let depsMap = targetMap.get(target)  
  11.     if(!depsMap){ 
  12.        targetMap.set(target, depsMap = new Map())  
  13.     } 
  14.     let dep = depsMap.get(key)  
  15.     if(!dep) {  
  16.       depsMap.set(key, dep = new Set());  
  17.     }  
  18.     dep.add(activeEffect)  
  19.   }  
  20.  
  21. function trigger(target, key) { 
  22.    let depsMap = targetMap.get(target)  
  23.   if(depsMap){  
  24.     let dep = depsMap.get(key)  
  25.     if(dep) {  
  26.       dep.forEach(effect => effect()) 
  27.     }  
  28.   } 
  29.  
  30. const reactiveHandler = {  
  31.   get(target, key, receiver) {  
  32.     const result = Reflect.get(target, key, receiver)  
  33.     track(target, key)  
  34.     return result  
  35.   },  
  36.   set(target, key, value, receiver) {  
  37.     const oldVal = target[key]  
  38.     const result = Reflect.set(target, key, value, receiver)  
  39.     if(oldVal !== result){  
  40.       trigger(target, key)  
  41.     }  
  42.     return result  
  43.   }  
  44.  
  45. function reactive(target) {  
  46.   return new Proxy(target, reactiveHandler)  
  47.  
  48. function ref(raw) {  
  49.   const r = {  
  50.     get value() {  
  51.       track(r, 'value')  
  52.       return raw  
  53.     },  
  54.     set value(newVal) {  
  55.       raw = newVal  
  56.       trigger(r, 'value)  
  57.     }  
  58.   }  
  59.   return r  

有同学可能就要问了,为什么不直接用第一种方法实现 ref,而是选择了比较复杂的第二种方法呢?

主要有三方面原因:

  1.  根据定义,ref 应该只有一个公开的属性,即 value,如果使用了 reactive 你可以给这个变量增加新的属性,这其实就破坏了 ref 的设计目的,它应该只用来包装一个内部的 value 而不应该作为一个通用的 reactive 对象;

      2.  Vue 3中有一个 isRef 函数,用来判断一个对象是 ref 对象而不是 reactive 对象,这种判断在很多场景都是非常有必要的;

      3.  性能方面考虑,Vue 3中的 reactive 做的事情远比第二种实现 ref 的方法多,比如有各种检查。

4. Computed

回到上节中最后的示例代码: 

  1. import { effect, reactive, ref } from "./reactive"  
  2. let obj = reactive({ a: 10, b: 20 })  
  3. let timesA = ref(0)  
  4. let sum = 0  
  5. effect(() => { timesA.value = obj.a * 10 }) 
  6. effect(() => { sum = timesA.value + obj.b }) 

看到 timesA 和 sum 两个变量,有同学就会说:“这不就是计算属性吗,不能像Vue 2一样用 computed 来表示吗?” 显然是可以的,看过 Vue composition API 的同学可能知道,Vue 3中提供了一个 computed 函数。

示例代码如果使用 computed 将变成这样: 

  1. import { effect, reactive, computed } from "./reactive"  
  2. let obj = reactive({ a: 10, b: 20 })  
  3. let timesA = computed(() => obj.a * 10)  
  4. let sum = computed(() => timesA.value + obj.b) 

现在的问题就是如何实现 computed ?

实现computed

我们拿 timesA 前后的改动来说明,思考一下 computed 应该是什么样的?

  1.  返回响应式对象,也许是 ref()
  2.  内部需要执行 effect 函数以收集依赖 
  1. function computed(getter) {  
  2.   const result = ref();  
  3.   effect(() => result.value = getter())  
  4.   return result  

现在测试一下示例代码: 

  1. import { effect, reactive, ref } from "./reactive"  
  2. let obj = reactive({ a: 10, b: 20 })  
  3. let timesA = computed(() => obj.a * 10)  
  4. let sum = computed(() => timesA.value + obj.b)  
  5. // 期望: timesA: 1000  sum: 1020 实际:timesA: 1000  sum: 1020  
  6. console.log(`timesA: ${timesA.value}  sum: ${sum.value}`)  
  7. obj.a = 100  
  8.  // 期望: timesA: 1000  sum: 1020  
  9. console.log(`timesA: ${timesA.value}  sum: ${sum.value}`) 

结果符合预期。

这样实现看起来很容易,实际上Vue 3中的 computed 支持传入一个 getter 函数或传入一个有 get 和 set 的对象,并且有其它操作,这里我们不做实现,感兴趣可以去看源码

现阶段完整代码

至此我们已经实现了一个简易版本的响应式库了,完整代码如下: 

  1. const targetMap = new WeakMap();  
  2. let activeEffect = null  
  3. function effect(eff) {  
  4.   activeEffect = eff  
  5.   activeEffect()  
  6.   activeEffect = null  
  7.  
  8. function track(target, key) {  
  9.   if(activeEffect){  
  10.     let depsMap = targetMap.get(target)  
  11.     if(!depsMap){  
  12.       targetMap.set(target, depsMap = new Map())  
  13.     } 
  14.     let dep = depsMap.get(key)  
  15.     if(!dep) { 
  16.        depsMap.set(key, dep = new Set());  
  17.     }  
  18.     dep.add(activeEffect)  
  19.   }  
  20.  
  21. function trigger(target, key) {  
  22.   let depsMap = targetMap.get(target)  
  23.   if(depsMap){ 
  24.      let dep = depsMap.get(key)  
  25.     if(dep) {  
  26.       dep.forEach(effect => effect())  
  27.     }  
  28.   }  
  29.  
  30. const reactiveHandler = {  
  31.   get(target, key, receiver) {  
  32.     const result = Reflect.get(target, key, receiver)  
  33.     track(target, key)  
  34.     return result  
  35.   },  
  36.   set(target, key, value, receiver) {  
  37.     const oldVal = target[key]  
  38.     const result = Reflect.set(target, key, value, receiver)  
  39.     if(oldVal !== result){  
  40.       trigger(target, key)  
  41.     }  
  42.     return result  
  43.   }  
  44.  
  45. function reactive(target) {  
  46.   return new Proxy(target, reactiveHandler)  
  47. function ref(raw) {  
  48.   const r = {  
  49.     get value() {  
  50.       track(r, 'value')  
  51.       return raw  
  52.     },  
  53.     set value(newVal) {  
  54.       raw = newVal  
  55.       trigger(r, 'value')  
  56.     }  
  57.   }  
  58.   return r  
  59.  
  60. function computed(getter) {  
  61.   const result = ref();  
  62.   effect(() => result.value = getter())  
  63.   return result  

尚存问题

我们现在的代码非常简易,有很多细节尚未实现,你都可以在源码中学习到,比如:

  •  操作一些内置的属性,如 Symbol.iterator、Array.length 等触发了 track 如何处理
  •  嵌套的对象,如何递归响应
  •  对象某个 key 对应的 value 本身是一个 reactive 对象,如何处理

你也可以自己尝试着实现它们。

本文完整内容见从零开始创建你的Vue 3 

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-09-27 06:29:47

Vue3 响应式原理Vue应用

2022-06-26 00:00:02

Vue3响应式系统

2019-07-01 13:34:22

vue系统数据

2021-01-22 11:47:27

Vue.js响应式代码

2021-12-02 05:50:35

Vue3 插件Vue应用

2017-08-30 17:10:43

前端JavascriptVue.js

2024-04-10 08:45:51

Vue 3Proxy对象监测数据

2023-06-01 08:27:30

SolidJS响应式函数

2016-12-21 14:35:46

响应式网页布局实现方法原理

2023-06-02 16:28:01

2022-01-19 18:05:47

Vue3前端代码

2012-02-21 16:39:29

响应式Web设计

2024-03-08 10:38:07

Vue响应式数据

2024-09-02 16:10:19

vue2前端

2020-12-01 08:34:31

Vue3组件实践

2023-02-06 08:39:01

PreactVue3响应式

2022-09-02 10:34:23

数据Vue

2021-12-09 08:49:14

Vue 3 Provide Inject

2024-07-08 08:43:19

2012-05-27 18:28:46

jQuery Mobi
点赞
收藏

51CTO技术栈公众号