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 属性的 get和set,来配合依赖追踪系统实现(track 和 trigger)实现响应式。
ref如果接收一个对象类型的数据内部会自动使用reactive。
也就是说当写下 const count = ref(0) 时,ref 函数在内部做的事情是:
- 创建一个空对象。
- 在空对象上定义一个属性为
value。 - 把传入的原始值
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 使用 ES6 的 Proxy 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';
610

被折叠的 条评论
为什么被折叠?



