深入剖析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 拦截来实现的,而是通过实例内部的属性访问器来完成的。

相关推荐
wearegogog1234 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars4 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤4 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·4 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°4 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
Irene19915 小时前
Vue3 <Suspense> 使用指南与注意事项
vue.js·suspense
qq_419854055 小时前
CSS动效
前端·javascript·css
烛阴5 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪6 小时前
markstream-vue实战踩坑笔记
前端
南村群童欺我老无力.6 小时前
Flutter应用鸿蒙迁移实战:性能优化与渐进式迁移指南
javascript·flutter·ci/cd·华为·性能优化·typescript·harmonyos