Vue3 中 ref 和 reactive 有什么区别?为什么会有区别?

  • 想要彻底弄清楚这个问题,需要从 Vue 的双向绑定依赖的底层技术来说起,Vue 实现双向绑定的核心在于如何对数据进行劫持,读取变量时 Vue 要知道读取了变量,而变量改变的时候也要知道去调用对应的副作用函数修改视图。

  • 在 Vue2 时期,底层实现的核心是 ES5 的 Object.defineProperty()​ 这个方法,在初始化时,会深度遍历 data 下所有的属性进行数据劫持,将每个属性转换为 getter 和 setter。这样当变量发生变化时,Vue 就可以监听到,并调用其对应的副作用方法作出相应的修改。

    但是这种方法也有很多缺点:

    1. 无法检测到属性的添加或移除
    2. 需要深度遍历 data 属性下的所有的值,将其转换为 getter 和 setter,性能不太好
    3. 对数组变化时的处理不够理想,直接通过数组索引赋值或修改数组长度时,无法检测到更改
  • 到了 Vue 3,双向绑定的底层实现改为使用 ES6 的 Proxy,解决了 Vue 2 时遗留的这些问题。但是这种方案也不是尽善尽美的,它也有自身的问题。

    基于 Proxy 的缺陷:

    • Proxy 只能对对象创建代理,基本类型的值就无能为力了。
    • 在 JavaScript 中,赋值操作或参数传递的过程中,引用类型传递的是内存地址,而基本类型是按值传递的。这意味着,对代理对象一个基本类型的值解构或者是将其作为参数传递时,获得的是另一个全新的值,它就是一个普通的变量,和原本的对象属性没什么关系。此时很明显,它失去了响应式。(Vue3 中可以[[借助 toRefs 方法]]来解决这个问题)
  • 上面的限制反应在 Vue 3 中,就是大体类似的几点:

    • reactive()​​​ 方法只支持对象(引用类型),不支持基本类型。

    • reactive()​​ 返回的响应式对象进行解构赋值,如果解构的是基本类型的值,响应式会丢失。因为解构出的就是一个普普通通的基本类型变量。

      <script setup lang="ts">
      const state = reactive({
        count: 0,
        subObj: {
          text: "Nice try!"
        },
      });
      const { count } = state; // 响应式丢失
      const { subObj } = state; // 保持响应式
      </script>
      
    • reactive()​​​ 返回的响应式对象的子属性作为参数传递,如果是基本类型,其响应式会丢失。

      <script setup lang="ts">
      const state = reactive({
        count: 0,
        subObj: {
          text: "Nice try!"
        },
      });
      
      function testBasisType(count) {
        count += 1;
        console.log(count);
      }
      
      function testReferenceType(msgObj) {
        msgObj.text = "Nice try again!";
        console.log(msgObj);
      }
      </script>
      
  • 上面的缺陷,究其原因都是 Proxy 无法代理基本类型的值,但是在开发过程中,肯定不能要求开发者一直将基本类型的值作为响应式对象的属性去访问,也太束手束脚了。

  • Vue3 使用了一个很简单的实现方法来解决这个问题 —— 使用 ref()​​​ 方法把基本类型的值包装为一个对象,然后通过对象的 value 属性去访问这个值。这样基本类型变成了引用类型,又回到上面谈论的那种情况了。

  • 所以 reactive 和 ref 最本质的区别在于:

    1. reactive 方法将引用类型的值转换为响应式对象(底层直接使用 Proxy 代理这个对象)。
    2. ref 方法会将基本类型的值包装为一个 ref 对象,再通过.value 去访问这个值(将基本类型的值包装为一个对象,底层依然是使用 Proxy 代理)
  • 以个人经验而言,弄清楚这两者为什么有区别很重要,因为整个 Vue 3 中的响应式核心都是基于这一套系统的,且衍生出了很多 工具函数 和概念。牢牢把握住核心,然后抽丝剥茧,掌握 Vue3 的响应式系统就会轻松很多。

  • 附录:

    • toRefs 方法解决对代理对象进行解构赋值,基本类型失去响应性的问题:

      const obj1 = reactive({
        bValue1: 1,
        bValue2: 2,
      });
      // 这样解构出来的值会失去响应性
      const { bValue1, bValue2 } = obj; 
      
      
      const obj2 = reactive({
        rValue1: 1,
        rValue2: 2,
      });
      // 结构出来的值都是 ref 对象,依然保持响应性
      const { rValue1, rValue2 } = toRefs(obj);
      rValue1.value += 1;  
      rValue2.value += 2;
      

      toRefs 原理:将一个响应式对象转换为普通对象,与此同时,将该对象的所有属性值都转换为 ref 对象,后续通过 .value 去访问(所以基本类型的值也能保证响应性了)。