理解 Vue3 的响应式基础
Q: Vue3 中 ref 和 reactive 有什么区别?什么时候该用哪个?
A: ref 是声明响应式数据的主要 API,可以持有任意类型的值(原始值、对象、数组等),通过 .value 访问内部值;reactive 只能用于对象类型,直接访问属性即可,但存在替换和结构丢失响应式的局限性。最佳实践是尽量使用 ref。
使用 ref 创建响应式数据
ref 是 Vue3 中最基础的响应式 API,可以将任意类型的数据包装为响应式引用:
<template>
<div>{{ name }}</div>
</template>
<script setup>
import { ref } from 'vue';
let name = ref('Bill');
console.log(name); // RefImpl 对象
console.log(name.value); // 'Bill'
setTimeout(() => {
name.value = 'Tom'; // 2 秒后自动更新视图
}, 2000);
</script>
要点:
- ref 返回的是一个响应式对象(RefImpl),需要通过
.value访问内部的具体值。 - 模板中会自动解包,不需要写
.value。 - ref 可以持有任意类型:原始值、对象、数组、Map、Set 等。
ref 持有对象
<template>
<div>{{ bill.name }}</div>
<div>{{ bill.age }}</div>
</template>
<script setup>
import { ref } from 'vue';
let bill = ref({
name: 'Bill',
age: 18
});
setTimeout(() => {
bill.value.name = 'Bill2';
bill.value.age = 20;
}, 2000);
</script>
ref 持有数组
<script setup>
import { ref } from 'vue';
let arr = ref([1, 2, 3]);
setTimeout(() => {
arr.value.push(4, 5, 6); // 触发响应式更新
}, 2000);
</script>
深层响应式
ref 创建的响应式数据默认是深层响应式的。当值是对象且内部有嵌套对象时,嵌套对象的属性变更同样会触发响应式:
<script setup>
import { ref } from 'vue';
let bill = ref({
name: 'Bill',
age: 18,
nested: { count: 1 }
});
setTimeout(() => {
bill.value.nested.count += 2; // 深层嵌套也会触发更新
}, 2000);
</script>
shallowRef:放弃深层响应式
如果数据结构很大或不需要深层响应,可以使用 shallowRef 来优化性能:
import { shallowRef } from 'vue';
const state = shallowRef({ count: 1 });
// 不会触发响应式更新
state.value.count += 2;
// 只有替换整个 .value 才会触发更新
state.value = { count: 2 };
响应式更新与 nextTick
响应式数据的修改会带来 DOM 的自动更新,但这个更新不是同步的。当修改响应式数据后立即读取 DOM,获取到的是旧值:
<template>
<div id="container">{{ count }}</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
let count = ref(1);
let container = null;
setTimeout(async () => {
count.value = 2;
// 此时 DOM 还未更新
await nextTick();
// 现在拿到的是最新的 DOM 值
console.log(container.innerText); // '2'
}, 2000);
onMounted(() => {
container = document.getElementById('container');
console.log(container.innerText); // '1'
});
</script>
nextTick 会等待下一次 DOM 更新周期完成后再执行回调,推荐使用 async/await 写法,逻辑更清晰。
使用 reactive 创建响应式数据
reactive 用于将对象转为响应式对象,不需要通过 .value 访问:
<template>
<div>{{ state.count1 }}</div>
<div>{{ state.nested.count2 }}</div>
</template>
<script setup>
import { reactive } from 'vue';
const state = reactive({
count1: 0,
nested: { count2: 0 }
});
setTimeout(() => {
state.count1++;
state.nested.count2 += 2; // 也是深层响应式
}, 2000);
</script>
reactive 也有对应的浅层版本 shallowReactive,不会将嵌套对象转为响应式。
ref 背后的原理
Vue3 的响应式底层是通过 Proxy API 实现的,但 Proxy 只能拦截对象操作,无法拦截原始值。那如果用户想把一个原始值(如字符串、数字)转为响应式怎么办?
两种方案:
- 让用户自己处理:用户自己将原始值包装成对象再使用 reactive — 体验不好。
- 框架层面处理:多提供一个 API,帮助用户简化操作 — 这就是 ref。
ref 的背后实际上也调用了 reactive:
- 如果传入的是原始值:内部使用 Object.defineProperty(getter/setter)
- 如果传入的是对象:内部调用 reactive API
这就是为什么 ref 能够处理任意类型的数据。
reactive 的三大局限性
虽然 ref 和 reactive 都能创建响应式数据,但 reactive 有三个重要的局限性:
1. 值类型受限
reactive 只能用于对象类型(object、array、map、set),不能用于原始值(string、number、boolean)。
2. 不能替换整个对象
let state = reactive({ count: 0 });
// 这会丢失响应式追踪!
state = reactive({ count: 1 });
上面的操作会导致原来的响应式引用丢失,因为 state 被重新赋值为了一个新的代理对象。
3. 对解构操作不友好
let state = reactive({ count: 0 });
let { count } = state; // 解构出来的是普通值,不是响应式的
count++; // 不会触发响应式更新
// 函数传参也同样会丢失响应式
func(state.count); // 传递的是普通值
最佳实践:尽量使用 ref
综合以上分析,推荐使用 ref 作为声明响应式数据的主要 API:
| 特性 | ref | reactive |
|---|---|---|
| 支持原始值 | ✅ | ❌ |
| 支持对象 | ✅ | ✅ |
| 可替换整个值 | ✅ | ❌ |
| 支持解构 | ✅(配合 toRefs) | ❌ |
| 模板自动解包 | ✅ | ✅ |
| 深层响应式 | ✅(默认) | ✅(默认) |
ref 的解包细节
ref 作为 reactive 对象的属性:自动解包
import { ref, reactive } from 'vue';
const name = ref('Bill');
const state = reactive({ name });
console.log(state.name); // 'Bill'(自动解包,无需 .value)
console.log(name.value); // 'Bill'
但如果 ref 作为 shallowReactive 对象的属性,则不会自动解包。
常用辅助 API
toRef 与 toRefs
import { reactive, toRef, toRefs } from 'vue';
const state = reactive({ count: 0, message: 'hello' });
// toRef:将响应式对象的某个属性转为 ref
const countRef = toRef(state, 'count');
console.log(countRef.value); // 0
// toRefs:将整个响应式对象的所有属性转为 ref
const stateRefs = toRefs(state);
console.log(stateRefs.count.value); // 0
console.log(stateRefs.message.value); // 'hello'
unref 与 toValue
import { ref, unref, toValue } from 'vue';
const countRef = ref(10);
unref(countRef); // 10(语法糖:isRef(val) ? val.value : val)
unref(20); // 20
// toValue 是 3.3 新增的,支持 getter 函数
toValue(() => 30); // 30
readonly:创建只读代理
import { ref, readonly } from 'vue';
const count = ref(0);
const countReadonly = readonly(count);
countReadonly.value++; // 会给出警告,不能修改
判断类 API
isRef(refVal); // 判断是否为 ref 对象
isReactive(state); // 判断是否为 reactive 代理
isProxy(obj); // 判断是否由 reactive/readonly 创建的代理
isReadonly(obj); // 判断是否为只读代理
总结
Vue3 响应式系统的核心是 ref 和 reactive。理解它们的工作原理和适用场景,是写好 Vue3 代码的基础:
- ref 是主要 API,支持任意类型,通过
.value访问,模板自动解包。 - reactive 仅支持对象类型,不能替换整体,对解构不友好。
- 搭配 toRef、toRefs、readonly 等辅助 API,可以灵活处理各种响应式场景。
- 牢记
nextTick,当需要在数据变更后获取最新 DOM 时使用。