深入剖析Vue.js源码:探寻ref的神奇之处,为什么它比reactive更强大?

当比较refreactive时,需要注意到ref相比reactive在支持基本数据类型的响应性方面具有优势,但需要使用.value来访问属性。此外,使用reactive重新分配一个新对象会导致丢失响应性,而ref不会受到此影响。

那么,ref是如何实现比reactive更强大的功能呢?让我们深入Vue.js源码,一探究竟!

另外,让我们首先介绍一下与 ref 非常相似的函数 shallowRef,因为它们的实现方式非常相似,我们将它们放在一起讨论

这两个函数在代理基本数据类型时没有区别,但它们的区别在于如何代理复杂数据类型。下面的示例将帮助我们理解它们之间的区别。

js 复制代码
        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 创建了一个对象 objshallowRef 创建了一个对象 obj1,它们都具有 foo 属性。然后,我们使用 effect 函数来监听这两个对象中的 foo 属性的变化。

当我们修改 objfoo 属性值时,第一个effect 中的回调会触发,并输出 2。这是因为 ref 创建的对象具有深层次的响应性,所以 foo 的变化被捕获了。

但当我们修改 obj1foo 属性值时,第二个effect 中的回调不会触发。这是因为 shallowRef 创建的对象没有建立深层次的响应关系,它只会捕获属性的直接变化,而不会递归地监视属性值的变化。

ok,接下来进入源码。(version:3.3.4)

js 复制代码
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

shallowRefref 函数都是由 createRef 这个工厂函数生成的。在 createRef 函数内部,首先会检查被代理对象是否已经是 ref 对象,如果是,则直接返回这个对象。否则,在工厂函数内部将创建一个 RefImpl 类的实例。

createRef 函数接受两个参数:rawValue 代表被代理对象,shallow 的作用是区分是要创建 shallowRef 还是 ref

RefImpl 类的构造函数将会接受这两个参数,这个类的构造函数将根据 shallow 参数的不同来设置代理对象的响应性方式,从而实现 refshallowRef 的不同行为。接下来进入构造函数

js 复制代码
    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;
    }
  1. __v_isShallow:区分ref,shallowRef
  2. __v_isRef:响应式对象是否由ref或shallowRef创建
  3. 如果是浅响应,实例上的_rawValue属性就是传入的value,但是如果是深响应,要考虑value是一个普通对象还是一个代理对象。
  4. toRaw的作用是如果value是一个普通对象,原样返回即可。如果已经是一个代理对象,那么它可以返回它的被代理对象,所以它返回的一定是一个原始对象.
  5. value是这个实例的访问器属性,当读取实例的value属性,触发get,需要收集依赖,并返回实例的_value属性

到了这里可以解决第一个疑问,即ref怎么实现对复杂数据类型的代理

访问value属性将会返回实例上的_value,当__v_isShallow为false,会返回后面的toReactive(value)

toReactive这个函数的作用是对value进行代理,如果value是简单数据类型,直接返回value.反之则会继续调用reactive函数对value进行代理,此时它的返回值是reactive生成的代理对象

结论:所以说ref还是调用了reactive来完成对复杂数据类型的代理。

js 复制代码
    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_isShallowtrue 时,newValue 无需额外处理。反之,需要考虑 newValue 可能是一个代理对象,因此必须将 newValue 设置为这个代理对象的原始值。

下面的 if 语句用于判断新旧值是否相同,如果它们相同,就无需触发通知。hasChanged 函数使用 Object.is 来进行比较,相比于 === 运算符,Object.isNaN的判断是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 是浅引用,它只关注对象自身的变化而不深入追踪对象内部属性的变化。

最后可以总结一下,对于 refvalue 属性的读取和修改,并不是通过 Proxy 拦截来实现的,而是通过实例内部的属性访问器来完成的。

相关推荐
明辉光焱10 分钟前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛30 分钟前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端
开心工作室_kaic1 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc
云空1 小时前
《InsCode AI IDE:编程新时代的引领者》
java·javascript·c++·ide·人工智能·python·php
晨曦_子画1 小时前
用于在 .NET 中构建 Web API 的 FastEndpoints 入门
前端·.net
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:在 PDF 文件中添加图像作为页面背景
前端·pdf·.net·spire.pdf
咔咔库奇1 小时前
ES6基础
前端·javascript·es6
Jiaberrr1 小时前
开启鸿蒙开发之旅:交互——点击事件
前端·华为·交互·harmonyos·鸿蒙
徐小夕2 小时前
Flowmix/Docx 多模态文档编辑器:V1.3.5版本,全面升级
前端·javascript·架构