前言
本文是Vue3硬核源码解析系列的第五篇文章,在之前文章中,我们了解到了reactive effect的源码实现原理,并抽丝剥茧输出了mini版本的reactive + effect,带领大家充分理解reactive的实现原理,同时我们也发现了reactive在使用上的一些局限性,比如无法代理基础类型。
正因为此,Vue3提供了另一个API ref,面对proxy无法代理基础类型数据的问题,ref又是如何实现其响应式的呢,本文将带领大家一起走进vue3源码世界,看看ref的实现原理
逻辑图
因为ref既可以传入基础类型,也可以传入复杂类型,所以其实现逻辑要比reactive更加复杂,并且依赖reactive。
前置知识
如果关于class get set已经很了解,请跳过前置知识
为了降低大家理解ref源码的难度,我们在正式阅读源码之前,先学习一下JavaScript的 class以及修饰符get set相关知识点
1 | class Obj { |
get: 被get修饰的方法,允许通过属性读取的方式,触发方法
set: 被set修饰的方法,允许通过属性赋值的方式,触发方法
当访问obj.value
的时候,会执行被get修饰的value(),打印log,并得到返回值‘张三’
当我们执行obj.value = ’李四‘
,进行赋值的时候,将会执行被set修饰的**value()**方法,打印log,并完成变量_value的赋值
看到这里,大家是否有点似曾相识的感觉,访问与赋值触发get set,和proxy代理的对象的get set很相似,大家能理解到这一点就足够了。
因为ref可以代理简单类型,同时也可以代理复杂类型,并且这两种情况下的响应式实现逻辑是完全不同的。
所以接下来,我们从这两个角度分别解读ref的源码实现,以及其核心逻辑。
首先我们看相对简单的基础类型场景,从源码的角度去了解ref是如何实现响应式的。
基础类型场景
案例
1 | let { ref, effect } = Vue |
上述代码现象:
页面初始化的时候显示“卖鱼强”
2s之后,name发生改变,变成了“狂飙强”。
通过现象与我们之前分析reactive的经验,这个我们可以将ref的实现分为三大模块
- 初始化
- 读取(依赖收集)
- 赋值(依赖触发)
初始化
packages/reactivity/src/ref.ts
1 | export function ref(value?: unknown) { |
通过源码分析,我们可以发现,ref的本质就是new RefImpl
我们ref传入的参数 原始对象被保存到_rawValue,同时将参数(“卖鱼强”)保存到-value中,便于后续的get set
读取
调用name.value
的时候,会触发RefImpl的**get value()**,方法内部返回最新的_value,完成读取。
1 | get value() { |
赋值
name.value
发生赋值的时候,会触发RefImpl的**set value()**方法,方法内部进行_value的赋值,完成数据更新。
1 | set value(newVal) { |
到此为止,ref的基础逻辑就完成,我们已经具备给ref赋值、读取的能力。
但是还不具备响应式的能力,接下来就让我们看看,ref的响应式系统是如何实现的。
依赖收集(trackRefValue)
根据我们解读reactive的源码经验,我们可以猜到,ref一定是在get中完成依赖收集的,事实也是如此。
而第一次ref的get是何时触发的呢?
答案是初始化时期的effect,effect触发后,内部fn被保存到activeEffect中,并触发fn,fn访问了name.value
,触发了ref的get行为,所以接下来我们前往RefImpl的get中,看看ref是如何完成依赖收集的。
1 | get value() { |
通过以上源码,我们可以发现,他们都公用了activeEffect部分的逻辑,但是ref收集依赖的方式与reactive是存在一些差别的
- reactive的依赖收集通过WeakMap完成,实现属性、变量与effect fn的绑定关系
- ref则通过自身实例内部的dep变量来保存所有相关的effect fn
依赖触发(triggerRefValue)
若干时间后,name.value
的值被修改,触发RefImpl的set value
1 | set value(newVal) { |
依赖触发的逻辑就非常简单了,set value的同时,获取当前ref的dep,并遍历dep中的依赖,依次执行,完成依赖触发。
小结
到此为止,我们基础类型场景的ref源码解读就结束了,我们简单做一下总结,
相比较于reactive,该场景下的逻辑要稍微简单一点,相关依赖(effect fn)被实例本身的dep管理,没有构建复杂的WeakMap对象。
ref与reactive的收集与触发的逻辑也不相同
- ref实际上是一个class RefImpl的实例
- 数据响应并不是通过proxy实现,而是通过class 的get set修饰符实现
- 依赖收集、触发并不是通过WeakMap实现,而是通过RefImpl实例中的变量dep实现
复杂类型场景
大家都知道ref不仅可以实现基础类型的响应式,还可以实现复杂类型的响应式,我们可以说ref是reactive的超集,那ref是如何实现既支持基础类型也支持复杂类型的呢?
接下来就让我们看看复杂类型场景下的ref是如何完成响应式的吧。
案例
1 | let { ref, effect } = Vue |
Ref初始化
首先依旧是进入ref函数中,开始new RefImpl,前面流程完全一致,所以直接我们进入RefImpl内部
1 | class RefImpl<T> { |
在constructor逻辑中,我们可以看到this._value = toReactive(value),而toReactive函数中,会首先识别value类型,如果不是object,原路返回,如果是object,将会被reactive函数处理,所以在该场景下,value将被reactive函数处理成proxy对象。
也就是说,此时ref内部的**_value实际上成了reactive**类型。
读取
初始化阶段,effect触发的时候,将会读取obj.value.name,,首先会访问量obj.value,触发ref的get方法。
obj.value获取完成后,继续去获取obj.value.name,而name已经在初始化阶段,被toReactive处理成了proxy,所以接下来,会再触发reactive的get,来获取name
也就是说,读取阶段,实际上触发了2次get,一次是ref的get value,一次是proxy的get,进而完成了变量的读取。
1 | get value() { |
赋值
若干时间后,obj.value.name发生set行为,首先依旧会触发ref的get,获取obj.value
,然后再触发reactive的set方法,完成name的赋值。
整个赋值过程,实际上分别触发了ref的get value,和proxy的set,进而完成变量的赋值
1 | //ref 本身的set在value为object,并且没有直接修改ref.value的情况下,不会被触发 |
到此为止,我们了解了ref在处理复杂对象时候的读取与赋值的逻辑。
读取:先触发ref的get,再触发proxy的get
赋值:先触发ref的get,再触发proxy的set
依赖收集
依赖收集是在get阶段进行完成,而通过上面的分析我们可以了解到,ref的get实际上其内部是两次get事件,所以我们分开来看。
ref的依赖收集(trackRefValue)
effect初始化阶段执行的时候,会读取obj.value.name
,首先会触发ref的get方法
1 | get value() { |
ref的get方法触发了trackRefValue,会在当前ref的dep中收集到effect,此处逻辑与ref为基础类型的逻辑一致。
proxy的依赖收集(track)
ref的的get完成后,紧接着触发了reactive的get,然后get内部通过WeakMap再次完成依赖收集(相关逻辑参考Vue3硬核源码解析系列(3) reactive + effect源码解析)。
我们会发现,在该阶段,我们内部实际上触发了2次依赖收集,effect fn被ref收集的同时,也被proxy收集了。
依赖触发
因为ref内部是一个对象,所以赋值也存在多种方式,这依赖触发存在多种方式
对象属性触发依赖
1 | obj.value.name = '狂飙强' |
这种不会破坏RefImpl初始化阶段其内部构建的proxy,仅修改已有proxy内部变量的值。
首先触发的是obj.value的get行为(此时没有effet在执行,不会发生依赖收集)。然后ref的get函数返回proxy对象 {name:'卖鱼强'}
,紧接着触发proxy的set,并完成依赖触发(proxy的依赖触发请看这里Vue3硬核源码解析系列(3) reactive + effect源码解析)。
对象触发依赖
1 | obj.value = { |
第二种方式首先触发obj.value的set行为,同时替换掉ref的值,注意这会破坏RefImpl初始化构建的_value的proxy,进而导致WeakMap中已有的依赖关系断裂
然后执行triggerRefValue,触发,ref本身在get阶段收集了相关effect fn,。
effect fn被触发后,再次触发ref的get,proxy的get,并帮助proxy又重建了与effect fn之间的依赖关系。
这就是为什么存在依赖收集2次的原因。
到此为止,我们的ref核心源码分析就全部完毕了。
关于ref的一些问题
Q:为啥一定要.value,不能干掉吗?
A:非常遗憾,value是去不掉的,因为ref依赖class get set 进行实现,在当前实现的场景下,可以简写为v,但是无法去除
Q:我是不是可以完全使用ref,不用reactive?
A:是的,可以完全使用ref,因为ref会根据你传入的类型,自动识别内部是否需要使用reactive,但是读过源码的同学知道ref在处理响应式系统中,存在重复收集依赖的场景,如果你有极致的性能要求,建议复杂类型依旧使用reactive完成,业务开发场景则无所谓。
如果还有其他问题,请评论区提问~
总结
通过对ref源码的阅读,我们可以察觉到,如果仅仅聚焦基础类型的ref,其实底层实现还是比较简单的,所以建议有兴趣的同学渐进式的阅读源码,先完成基础类型场景的源码解读,再进行复杂类型的源码解读,这样事半功倍~
如果有任何问题,请评论区留言~
下一个阶段,我将手摸手带大家完成mini版本vue3 ref API,帮助大家深入理解ref~