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

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

当比较refreactive时,需要注意到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 创建了一个对象 objshallowRef 创建了一个对象 obj1,它们都具有 foo 属性。然后,我们使用 effect 函数来监听这两个对象中的 foo 属性的变化。

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

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

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

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 的不同行为。接下来进入构造函数

    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来完成对复杂数据类型的代理。

    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);不用管,因为它只是为了处理边界情况,纠结于这些细枝末节会十分痛苦。。。。面试也不会问,哈哈。

image.png

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

相关文章
|
13天前
|
JavaScript
JS实现简单的打地鼠小游戏源码
这是一款基于JS实现简单的打地鼠小游戏源码。画面中的九宫格中随机出现一个地鼠,玩家移动并点击鼠标控制画面中的锤子打地鼠。打中地鼠会出现卡通爆破效果。同时左上角统计打地鼠获得的分数
30 1
|
4天前
|
前端开发 JavaScript
用HTML CSS JS打造企业级官网 —— 源码直接可用
必看!用HTML+CSS+JS打造企业级官网-源码直接可用,文章代码仅用于学习,禁止用于商业
31 1
|
10天前
|
JavaScript
JS趣味打字金鱼小游戏特效源码
hi fish是一款打字趣味小游戏,捞出海里的鱼,捞的越多越好。这款游戏用于电脑初学者练习打字。初学者可以根据自己的水平设置游戏难度。本段代码可以在各个网页使用,有需要的朋友可以直接下载使用,本段代码兼容目前最新的各类主流浏览器,是一款非常优秀的特效源码!
16 3
|
12天前
|
JavaScript
JS鼠标框选并删除HTML源码
这是一个js鼠标框选效果,可实现鼠标右击出现框选效果的功能。右击鼠标可拖拽框选元素,向下拖拽可实现删除效果,简单实用,欢迎下载
25 4
|
12天前
|
JavaScript
js实现简洁实用的网页计算器功能源码
这是一款使用js实现简洁实用的网页计算器功能源码。可实现比较基本的加减乘除四则运算功能,界面简洁实用,是一款比较基本的js运算功能源码。该源码可兼容目前最新的各类主流浏览器。
21 2
|
14天前
|
JavaScript
JS实现的虚化雪景动态背景特效源码
JS实现的虚化雪景动态背景特效源码是一段基于JS实现的虚化雪景动态背景动画效果代码,非常有意思,欢迎对此特效感兴趣的朋友前来下载参考。
24 4
|
11天前
|
移动开发 HTML5
html5+three.js公路开车小游戏源码
html5公路开车小游戏是一款html5基于three.js制作的汽车开车小游戏源代码,在公路上开车网页小游戏源代码。
34 0
html5+three.js公路开车小游戏源码
|
11天前
|
JavaScript
JS趣味打字金鱼小游戏特效源码
hi fish是一款打字趣味小游戏,捞出海里的鱼,捞的越多越好。这款游戏用于电脑初学者练习打字。初学者可以根据自己的水平设置游戏难度。本段代码可以在各个网页使用,有需要的朋友可以直接下载使用,本段代码兼容目前最新的各类主流浏览器,是一款非常优秀的特效源码!
21 0
JS趣味打字金鱼小游戏特效源码
|
13天前
|
JavaScript
js实现的精美彩色tab选项卡切换特效源码
js实现的精美彩色tab选项卡切换特效源码是一段基于JS实现的文件夹tab选项卡切换效果,拥有彩色、单色两种选择,点击标签选项卡可实现相应的变色效果,非常有意思,欢迎对此段代码感兴趣的朋友前来下载使用。
23 2
|
14天前
|
JavaScript
js动画循环播放特效源码(上班族的一天)
js动画循环播放特效是一段实现了包含形象的卡通小人吃、睡、电脑工作的网页动画,js循环动画,简单的画面设计。非常丝滑有意思,欢迎对此代码感兴趣的朋友前来下载参考。
25 2