当比较
ref
与reactive
时,需要注意到ref
相比reactive
在支持基本数据类型的响应性方面具有优势,但需要使用.value
来访问属性。此外,使用reactive
重新分配一个新对象会导致丢失响应性,而ref
不会受到此影响。那么,
ref
是如何实现比reactive
更强大的功能呢?让我们深入Vue.js源码,一探究竟!另外,让我们首先介绍一下与
ref
非常相似的函数shallowRef
,因为它们的实现方式非常相似,我们将它们放在一起讨论
这两个函数在代理基本数据类型时没有区别,但它们的区别在于如何代理复杂数据类型。下面的示例将帮助我们理解它们之间的区别。
let obj = ref({
foo: 1 });
let obj1 = shallowRef({
foo: 1 });
effect(() => {
console.log(obj.value.foo)
})
effect(() => {
console.log(obj1.value.foo)
})
obj.value.foo = 2
obj1.value.foo = 3
//打印的结果是1,1,2
在上面的示例中,我们首先使用 ref
创建了一个对象 obj
和 shallowRef
创建了一个对象 obj1
,它们都具有 foo
属性。然后,我们使用 effect
函数来监听这两个对象中的 foo
属性的变化。
当我们修改 obj
的 foo
属性值时,第一个effect
中的回调会触发,并输出 2
。这是因为 ref
创建的对象具有深层次的响应性,所以 foo
的变化被捕获了。
但当我们修改 obj1
的 foo
属性值时,第二个effect
中的回调不会触发。这是因为 shallowRef
创建的对象没有建立深层次的响应关系,它只会捕获属性的直接变化,而不会递归地监视属性值的变化。
ok,接下来进入源码。(version:3.3.4)
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
shallowRef
和 ref
函数都是由 createRef
这个工厂函数生成的。在 createRef
函数内部,首先会检查被代理对象是否已经是 ref
对象,如果是,则直接返回这个对象。否则,在工厂函数内部将创建一个 RefImpl
类的实例。
createRef
函数接受两个参数:rawValue
代表被代理对象,shallow
的作用是区分是要创建 shallowRef
还是 ref
。
RefImpl
类的构造函数将会接受这两个参数,这个类的构造函数将根据 shallow
参数的不同来设置代理对象的响应性方式,从而实现 ref
和 shallowRef
的不同行为。接下来进入构造函数
constructor(value: T, public readonly __v_isShallow: boolean) {
this.__v_isShallow = __v_isShallow
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this);
return this._value;
}
__v_isShallow
:区分ref,shallowRef__v_isRef
:响应式对象是否由ref或shallowRef创建- 如果是浅响应,实例上的
_rawValue
属性就是传入的value,但是如果是深响应,要考虑value是一个普通对象还是一个代理对象。 toRaw
的作用是如果value是一个普通对象,原样返回即可。如果已经是一个代理对象,那么它可以返回它的被代理对象,所以它返回的一定是一个原始对象.value是这个实例的访问器属性
,当读取实例的value属性,触发get,需要收集依赖,并返回实例的_value
属性
到了这里可以解决第一个疑问,即ref怎么实现对复杂数据类型的代理
。
访问value
属性将会返回实例上的_value
,当__v_isShallow
为false,会返回后面的toReactive(value)
toReactive
这个函数的作用是对value进行代理,如果value是简单数据类型,直接返回value.反之则会继续调用reactive函数对value进行代理,此时它的返回值是reactive生成的代理对象
结论:所以说ref还是调用了reactive来完成对复杂数据类型的代理。
set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
对于设置set来说,isShallow(newVal) || isReadonly(newVal);
不用管,因为它只是为了处理边界情况,纠结于这些细枝末节会十分痛苦。。。。面试也不会问,哈哈。
当 __v_isShallow
为 true
时,newValue
无需额外处理。反之,需要考虑 newValue
可能是一个代理对象,因此必须将 newValue
设置为这个代理对象的原始值。
下面的 if
语句用于判断新旧值是否相同,如果它们相同,就无需触发通知。hasChanged
函数使用 Object.is
来进行比较,相比于 ===
运算符,Object.is
对NaN
的判断是true。接下来,将新值赋给 _rawValue
,然后将经代理处理后的 newValue
赋值给 _value
,并派发通知
这也可以解释第二个问题:给ref 重新分配一个普通对象不会导致失去响应性
。这是因为新分配的值会经过 toReactive
处理,然后再赋给 _value
,而 get
方法返回的就是 _value
,也就是这个值已经经过响应式处理的数据。
可以发现一个规律:
使用shallowRef,即__v_isShallow为true时.在构造函数中不需要判断传人的值是不是原始对象还是代理对象.直接赋给_rawValue和_value.
而当__v_isShallow为false时,如果传入的是代理对象,将找到对应的原始对象赋值给_rawValue.被reactive处理之后的具有响应式的值将赋给_value
分离 _rawValue
和 _value
使得在 ref
的内部逻辑中能够明确区分原始值和经过响应式处理的值,
考虑一种情况,我们将一个
ref
传入一个reactive
代理对象,然后尝试设置新值,而这个新值刚好是与代理对象的原始值相同。此时,是否还需要触发通知(trigger
)呢?答案是否定的。即使新值与代理对象的原始值相同,Vue 3 仍会将其转化为代理对象并赋值给
_value
,因此它们仍然引用相同的代理对象,这就意味着不需要触发通知。然而,需要注意的是,如果是将一个
shallowRef
传入一个reactive
代理对象,然后将这个代理对象对应的原始值设置给shallowRef
,这将会触发通知,因为shallowRef
是浅引用,它只关注对象自身的变化而不深入追踪对象内部属性的变化。
最后可以总结一下,对于 ref
的 value
属性的读取和修改,并不是通过 Proxy
拦截来实现的,而是通过实例内部的属性访问器
来完成的。