总览
上一篇文章我们介绍了reactive实现响应式的原理,其核心是利用Proxy拦截了对象的读取和写入操作,但是Proxy拦截的目标对象一定要是对象类型的数据(数组也是对象),对于基本数据类型的字符串、数值等数据就无法使用Proxy实现数据的拦截操作了。那么Vue3又是如何去实现基本数据类型的响应式读取和写入呢?为什么ref又一定要通过.value的方式读取呢?
这就要引出我们今天的主人公:Ref。
我们可以在packages\reactivity\src\ref.ts中找到ref函数的实现:
可以看到,ref函数中就做了一件事:调用createRef函数,并把结果返回。
createRef
在同一个文件中可以找到createRef函数的实现:
由于我们传入的并非一个ref类型的值,所以会执行
RefImpl构造函数,并返回生成的实例,所以ref函数返回的是一个RefImpl实例。
RefImpl
我们可以看到RefImpl实例有两个属性(_rawValue,_value)以及两个属性访问器(getter/setter)。
值得注意的是,_value在赋值的时候会调用toReactive函数,并将结果赋值给_value。
toReactive函数会判断ref函数的参数是否是一个对象,如果是一个对象,则调用reactive函数,否则就直接返回传入的参数。
所以,当我们调用ref函数并传入一个对象时,ref函数内部会调用reactive函数,则此时的RefImpl实例中的_value是一个proxy代理对象。而当我们传入的是一个基本类型的数据时,RefImpl实例中的_value就是我们传入的参数本身。所以ref实现响应式的原理要分情况讨论。
响应式原理
参数为对象
我们写一个example:
import { ref, effect } from 'vue'
const obj = ref({
name: '法外狂徒张三'
})
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
})
上面的分析我们知道,ref函数返回的是RefImpl实例,即上述的obj是一个RefImpl实例,而我们传入的是对象时,RefImple._value是一个由reactive函数返回的proxy对象。
在effect函数中,我们有获取obj.value.name的操作,这段代码相当于下面的拆分:
const o = obj.value
return o.name
上述代码中obj.value实际会触发RefImpl实例中的getter属性访问器。
getter属性访问器最终返回
RefImpl._value,而这是一个由reactive函数生成的proxy对象。所以无论当我们去访问亦或是修改obj.value.name的值,实际都是触发了这个proxy对象的拦截操作,关于reactive函数和其生成的proxy代理对象,可参考这篇文章。
参数为基本数据类型
同样写一个example:
import { ref, effect } from 'vue'
const obj = ref('法外狂徒张三')
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
})
上述的obj同样是一个RefImpl实例,而我们当传入的是基本数据类型时,RefImple._value就是这个参数本身,此例中是一个字符串。
所以在effect函数内部访问obj.value时,返回的是ref参数本身,即,一个基本数据类型。
而当执行obj.value = '李四'时,我们触发的是RefImpl实例中的另一个属性访问器:Setter
在Setter中,做了两件事:
1、判断_value是否发生了变化,如果是,则重新赋值_value
2、执行依赖
这样ref就实现了对基本数据类型的响应式。
总结
1、ref函数返回的一个RefImpl实例
2、当传入的是一个复杂数据类型时,ref是调用reactive函数完成响应式
3、当传入的是一个基本数据类型时,ref是借用RefImpl两个属性访问器实现响应式的,这也是为什么ref一定要通过.value的方式来取值的原因,因为只有通过.value的方式才能触发getter/setter属性访问器。
最后,我们结合reactive那一文的图,将其补充完整: