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

相关文章
|
6天前
|
JavaScript
Vue——Vue v2.7.14 源码阅读之代码目录结构【一】
Vue——Vue v2.7.14 源码阅读之代码目录结构【一】
13 0
|
4天前
|
前端开发
StringBoot+Vue实现游客或用户未登录系统前、可以浏览商品等信息、但是不能购买商品或者加入购物车等操作。登录系统显示用户的登录名(源码+讲解)
这篇文章介绍了使用StringBoot+Vue实现用户登录状态判断的方法,包括前端加载用户信息和后端设置session的源码示例。
|
1月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
60 2
|
5天前
|
JavaScript 开发工具 git
Vue学习之--------脚手架的分析、Ref属性、Props配置(2022/7/28)
这篇文章分析了Vue脚手架的结构,并详细讲解了`ref`属性和`Props`配置的基础知识、代码实现和测试效果,展示了如何在Vue组件中使用`ref`获取DOM元素或组件实例,以及如何通过`Props`传递和接收外部数据。
Vue学习之--------脚手架的分析、Ref属性、Props配置(2022/7/28)
|
11天前
|
JavaScript API 开发者
Vue 3 为什么同时需要 Ref 和 Reactive?
Vue 3 为什么同时需要 Ref 和 Reactive?
|
11天前
|
前端开发
【前端】校园二手书交易系统javascript+css+html (源码)【独一无二】
【前端】校园二手书交易系统javascript+css+html (源码)【独一无二】
|
1月前
|
JavaScript 前端开发 程序员
《JavaScript权威指南第7版》中文PDF+英文PDF+源代码 +JavaScript权威指南(第6版)(附源码)PDF下载阅读分享推荐
JavaScript是Web标准语言,广泛应用于各类浏览器,造就了其最广泛部署的地位。Node.js的兴起扩展了JavaScript的使用场景,使其成为开发者首选语言。无论新手还是经验丰富的程序员,都能受益于学习JavaScript。[《JavaScript权威指南第7版》资源链接](https://zhangfeidezhu.com/?p=224)
67 5
《JavaScript权威指南第7版》中文PDF+英文PDF+源代码 +JavaScript权威指南(第6版)(附源码)PDF下载阅读分享推荐
|
10天前
|
监控 数据可视化 前端开发
【前端】政务服务大数据可视化监控平台(源码+html+css+js)
【前端】政务服务大数据可视化监控平台(源码+html+css+js)
|
17天前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
1月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
54 3