讲解
响应式是 Vue 3 一切能力的地基,而它对外暴露的两个核心 API 就是 ref 和 reactive。reactive 接收一个对象,返回它的响应式代理:访问和修改属性都会被追踪,模板或副作用函数用到这些属性时自动建立依赖。ref 则更进一步,可以把任意值(数字、字符串、布尔,甚至对象)包成响应式:它返回一个 { value } 对象,通过 .value 读写。
为什么要造两个 API?根源在 JavaScript 的限制:Proxy 只能拦截对象的属性访问,拦截不了「把变量重新赋值为另一个数字」这种操作。所以基本类型必须包一层对象(这就是 ref 的 .value),靠拦截 .value 的 get/set 实现追踪。对象则两种都行,社区约定俗成的偏好是:基本类型用 ref,对象也优先用 ref(因为 ref 可以整体替换 .value,reactive 不能换根引用),只有需要保持固定引用的场景才用 reactive。
两个高频细节。第一,模板里会自动解包:模板中写 {{ count }} 而不是 {{ count.value }},但在 <script setup> 的 JavaScript 代码里必须写 count.value。第二,解构会丢响应式:const { count } = state 把值拷贝出来了,之后 state.count 变化不会同步;想安全解构 reactive 对象要用 toRefs 或 toRef。
还要知道 Vue 提供了一组「响应式工具函数」判断状态:isRef、isReactive 检查身份;unref 是「是 ref 就取 .value,否则原样返回」的语法糖,在组合式函数里特别常用;readonly 可以创建只读视图,防止下游误改上游状态。
示例
用纯 JavaScript 把 ref/reactive 的语义精确模拟出来,理解「为什么 ref 要有 .value」(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 迷你依赖追踪:谁在读,写入时通知谁
const watchers = [];
function track(label) {
for (const w of watchers) w.log.push('读取 ' + label);
}
function trigger(label) {
for (const w of watchers) w.log.push('更新 ' + label);
}
// ref:基本类型包一层 { value },拦截 value 的读写
function ref(raw) {
return {
get value() {
track('ref.value');
return raw;
},
set value(v) {
raw = v;
trigger('ref.value');
},
};
}
// reactive:Proxy 拦截对象属性
function reactive(obj) {
return new Proxy(obj, {
get(t, k) {
track('reactive.' + String(k));
return t[k];
},
set(t, k, v) {
t[k] = v;
trigger('reactive.' + String(k));
return true;
},
});
}
const watcher = { log: [] };
watchers.push(watcher);
const count = ref(0);
const state = reactive({ name: 'Vue' });
count.value = 1; // 触发更新
const doubled = count.value * 2; // 触发读取
state.name = 'Vue 3';
assert.strictEqual(doubled, 2);
assert.deepStrictEqual(watcher.log, [
'更新 ref.value',
'读取 ref.value',
'更新 reactive.name',
]);
// 解构丢响应式:拷走的是值,不是联系
const raw = { n: 1 };
const { n } = raw;
raw.n = 99;
assert.strictEqual(n, 1); // n 停在旧值
console.log('依赖追踪日志:', watcher.log.join(' | '));
console.log('解构拷贝后 n =', n, '(不随原对象更新)');
对应到真实组件,用法极简:
<script setup>
import { ref, reactive } from 'vue';
const count = ref(0); // 基本类型用 ref
const user = reactive({ name: '小明', age: 18 }); // 对象可用 reactive
function birthday() {
user.age++; // reactive 直接改属性
count.value++; // ref 在 JS 里用 .value
}
</script>
<template>
<p>{{ user.name }} 今年 {{ user.age }} 岁,点了 {{ count }} 次</p>
<button @click="birthday">过生日 + 点赞</button>
</template>
常见坑
- 在 JS 里忘记 .value:模板自动解包惯坏了手感,回到
<script setup>里count++静默无效(改的是 ref 对象本身不存在的属性)。 - 解构 reactive 对象:
const { name } = user之后 name 就不再响应,需要解构就用 toRefs。 - 给 ref 赋错层级:
count.value = { a: 1 }之后要count.value.a,层级嵌错是新手高频 bug。 - 整体替换 reactive 引用:
state = fetchNew()会直接报赋值错误(const)或丢响应(let 重新绑定),reactive 只能改属性不能换引用,要整体替换请用 ref。 - 过度使用 reactive:统一用 ref 反而更少心智负担,团队规范里「默认 ref」是常见约定。
小结
基本类型必须 ref,对象两者皆可、推荐 ref;JS 里 .value、模板里自动解包;解构丢响应式,需要解构用 toRefs。下一章看基于响应式推导状态的第一个利器:computed。