Vue 3 响应式 API 深度拆解:ref 与 reactive 到底差在哪?

1. ref vs reactive:五个核心纬度的深度对比

对比维度

ref

reactive

1. 接受的参数类型

任意类型 (All types)

只能是对象类型 (Objects, Arrays, Map, Set etc.)

2. 底层实现原理

getter / setter + 依赖追踪

Proxy (ES6)

3. 访问/修改数据的方式

必须通过 .value 属性 (in <script setup>)

直接操作对象属性

4. 模板中的使用

自动解包 (不需要 .value)

直接使用

5. 重新赋值的差异

可以被整个重新赋值

不可以,会失去响应性

 

2. 接受的参数:灵活性 vs 局限性

2.1. ref

非常灵活,可以把任何类型的数据传递给他。

const name = ref('小王');         // String
const age = ref(23);             // Number
const loading = ref(true);         // Boolean
const user = ref({ id: 1 });      // Object
const list = ref([1, 2, 3]);       // Array

2.2. reactive

有局限性只能接受对象类型的数据,如果传入原始类型,它不会工作还会收到一个警告。

const user = reactive({ id: 1 });   // Object
const list = reactive([1, 2, 3]);    // Array 

const name = reactive('张三');   // 无效, 会有警告

我们知道,Vue 3 的响应式核心是 Proxy。但 Proxy 有一个天生的局限:它只能代理对象(Object)或数组(Array),无法代理原始类型的值(比如 String, Number, Boolean)。

let name = "张三";
let proxiedName = new Proxy(name, {}); // 报错:Cannot create proxy with a non-object as target or handler

let user = {name: "张三"};
let proxiedUser = new Proxy(user, {});

正是因为有了这个局限性,才有了 ref。

3. 底层原理实现:包装盒 vs 全面代理

3.1. ref

ref 的核心是创建一个“包装盒”对象,这个对象有一个 .value 的属性,Vue 通过劫持这个 .value 属性的 getset,来配合依赖追踪系统实现(track 和 trigger)实现响应式。

  • ref 如果接收一个对象类型的数据内部会自动使用 reactive

也就是说当写下 const count = ref(0) 时,ref 函数在内部做的事情是:

  1. 创建一个空对象。
  2. 在空对象上定义一个属性为 value
  3. 把传入的原始值0存放到 conunt.value里。

所以,ref(0) 返回的 count,它的结构其实是这样的:

// ref(0) 的返回值,在概念上是这样的
{
  value: 0
}

它的核心实现,是把原始类型的值包装成一个包含 .value 属性的对象

// 引入Vue响应式系统的核心:依赖收集 和 触发更新
import { track, trigger } from '@vue/reactivity';

function ref(value) {
  // 1. 将传入的值包装成一个对象
  const refObject = {
    // 这是一个内部属性,用来存储真实的值
    _value: value,

    // 2. 利用 getter/setter 对 .value 属性进行劫持
    get value() {
      // 当读值时
      console.log('Getter:');
      // track() 函数会记录下来需要得到哪个值
      track(refObject, 'value');
      return this._value;
    },

    set value(newValue) {
      // 当给 .value 赋值的时候
      console.log('Setter');
      // 只有在新值和旧值不同时,才需要更新
      if (newValue !== this._value) {
        this._value = newValue;
        // trigger() 函数会通知更新
        trigger(refObject, 'value');
      }
    }
  };

  return refObject;
}

3.2. reactive

reactive 使用 ES6Proxy API,传入的对象以及所有的嵌套属性的任何读写操作,都会被 Proxy 拦截。

  • 不需要像 ref 那样额外包装。

4. 访问/修改数据的方式

4.1. ref

必须通过 .value 属性来访问和修改内部的值。

const count = ref(0);

console.log(count.value); // 0 (读取)

function increment() {
  count.value++; // (修改)
}

4.2. reactive

操作起来和普通的 JS 对象完全一样

const state = reactive({ count: 0 });

console.log(state.count); // 0 (读取)

function increment() {
  state.count++; // (修改)
}

5. 模版的使用

5.1. ref

为了减轻 ref 在脚本中带来的 .value 心智负担,Vue 在模板(<template>中做了一个非常智能的优化:自动解包 (unwrapping)

<template>
  <p>{{ count }}</p> <!-- 无需 .value -->
  <button @click="count++">Increment</button> <!-- 无需 .value -->
</template>
<script setup>
  import { ref } from 'vue';
  const count = ref(0);
</script>

5.2. reactive

直接访问即可

<template>
  <p>{{ state.count }}</p>
  <button @click="state.count++">Increment</button>
</template>
<script setup>
  import { reactive } from 'vue';
  const state = reactive({ count: 0 });
</script>

6. 重新赋值的差异

这是两者在实际开发中一个非常重要的区别,也是很多人优先推荐使用 ref 的原因

6.1. ref

把一个 ref 对象变量整个地替换一个新的值,依然保持响应

let user = ref({ name: 'A' });

// 整个对象被替换,视图会自动更新
user.value = { name: 'B' }; 

6.2. reactive

不能直接替换一个 reactive 对象,这样会使用变量与 Proxy 失去连接,从而失去响应式

let user = reactive({ name: 'A' });

// 错误的做法!
user = { name: 'B' }; // 这样操作后,user 不再是响应式的了

// 正确的做法是,修改原对象的属性
Object.assign(user, { name: 'B' });
// 或者逐个赋值
// user.name = 'B';
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值