响应式进阶
toRefs、shallowRef、readonly、customRef 等 API 的适用场景与注意点。前置:依赖收集。
以下是 Vue 3 响应式系统的高级主题详解,涵盖响应式丢失的成因与补救、浅响应式、只读响应式及自定义响应式。
一、响应式丢失问题
1. 成因:解构 reactive 对象
reactive 返回的是一个 Proxy 对象,其属性访问会被拦截。但若将该对象的属性解构到普通变量,这些变量就只是初始值的快照,不再具有响应性。
import { reactive } from "vue";
const state = reactive({ count: 0, name: "Alice" });
let { count, name } = state; // count 和 name 此时是普通数字/字符串
// 修改 state.count 会触发视图更新
state.count++; // ✅ 响应式
// 但直接修改解构后的 count 不会影响原状态,也不会触发更新
count++; // ❌ 失去响应式
同样的问题也发生在将 reactive 对象的属性作为参数传递给函数、或从组合式函数中返回解构对象时。
2. 解决方案:toRefs 与 toRef
toRefs
将 reactive 对象转换为一个普通对象,该对象的每个属性都是指向原对象对应属性的 ref。这样解构后得到的每个属性都是 ref 对象,保持响应连接。
import { reactive, toRefs } from "vue";
const state = reactive({ count: 0, name: "Alice" });
const stateAsRefs = toRefs(state);
// stateAsRefs 结构: { count: Ref(0), name: Ref('Alice') }
// 解构后仍然是 ref
const { count, name } = stateAsRefs;
count.value++; // ✅ 等价于 state.count++,触发更新
使用场景:将整个 reactive 对象的属性解构到多个变量,或从组合式函数返回响应式状态时。
toRef
为 reactive 对象的单个属性创建一个 ref,保持响应连接。与 toRefs 的区别是只处理一个属性。
import { reactive, toRef } from "vue";
const state = reactive({ count: 0 });
const countRef = toRef(state, "count");
countRef.value++; // ✅ 等价于 state.count++
使用场景:仅需要将某一个属性变为 ref 并保持响应性,例如传递给可接收 ref 的组合式函数。
3. 原理简述
toRef / toRefs 内部创建了一个指向源对象属性的访问器,其 .value 的 getter/setter 直接操作源对象的对应属性,并在读写时触发依赖收集与更新。
function toRef(target, key) {
return {
get value() {
return target[key];
},
set value(newVal) {
target[key] = newVal;
},
};
}
注意:toRef 即使源属性当前不存在,也会创建一个可写的 ref,当设置值时会在源对象上新增属性(响应式)。
二、浅响应式(shallowRef / shallowReactive)
1. shallowReactive
只拦截对象的第一层属性,深层属性不会被转换为响应式。适用于大型数据结构,但只需监听顶层变化。
import { shallowReactive } from "vue";
const state = shallowReactive({
user: { name: "Alice" }, // 深层对象,不是响应式的
count: 0, // 第一层,响应式
});
state.count++; // ✅ 触发更新
state.user.name = "Bob"; // ❌ 不会触发更新,因为 user 不是响应式
性能收益:避免深度递归代理,提升初始化和访问速度,节省内存。 风险:修改深层对象时不会触发视图更新,容易造成 bug。因此仅适用于结构扁平、深层属性不会变更的场景(如静态配置)。
2. shallowRef
只对 .value 的访问进行响应式追踪,如果 .value 是一个对象,不会递归将其内部属性转为响应式。
import { shallowRef } from "vue";
const data = shallowRef({ count: 0 });
data.value.count = 1; // ❌ 不会触发更新
data.value = { count: 1 }; // ✅ 替换整个 value 会触发更新
典型应用:存储大对象但不需要其内部属性的响应式(如图表配置、第三方实例)。配合 triggerRef 可手动强制更新。
3. 何时使用浅响应式?
- 性能敏感的大型列表/树,仅需监听引用变化。
- 第三方库对象(如
ECharts实例)不希望被代理。 - 与
markRaw标记永久不可代理的对象配合。
三、只读响应式(readonly、shallowReadonly)
1. readonly
接受一个响应式对象(或普通对象),返回一个只读代理。任何修改操作(属性赋值、删除)都会在开发模式下抛出警告。
import { reactive, readonly } from "vue";
const original = reactive({ count: 0 });
const copy = readonly(original);
copy.count++; // ⚠️ 警告: Set operation on key "count" failed: target is readonly.
original.count++; // ✅ 可以修改原对象,copy 也会相应变化(因为是同一个底层对象)
应用场景:
- 保护组件的 props(内部不应修改)。
- 向外部提供不可变的状态(如 Pinia store 的 getter 返回只读状态)。
- 防止意外修改共享状态。
2. shallowReadonly
只确保对象的第一层属性只读,深层属性仍可修改。
const state = shallowReadonly({ user: { name: "Alice" }, version: 1 });
state.version = 2; // 警告
state.user.name = "Bob"; // ✅ 允许(深层不是只读)
应用场景:深层结构巨大但只需保护顶层引用的场合。
3. 嵌套只读
readonly 会递归地将所有嵌套对象也变为只读,而 shallowReadonly 只影响第一层。
四、自定义响应式(定制 ref)
通过 customRef 可以创建带有自定义逻辑的 ref,例如实现防抖、节流、本地存储同步等。
1. customRef 基本用法
import { customRef } from "vue";
function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => ({
get() {
track(); // 依赖收集
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger(); // 触发更新
}, delay);
},
}));
}
2. 使用示例
const text = useDebouncedRef("hello", 500);
// 在模板中绑定 input,输入停止 500ms 后才会更新 text
3. 更多应用场景
- 本地存储同步 ref:
get时从 localStorage 读取,set时写入。 - 节流 ref:限制高频赋值频率。
- 验证 ref:在 setter 中进行数据校验,无效值不更新并报错。
- 联动 ref:一个 ref 的值变化会同步修改另一个数据。
4. 与普通 ref 的区别
- 普通
ref基于trackRefValue/triggerRefValue内置逻辑。 customRef完全让开发者控制何时track()收集依赖,何时trigger()派发更新,适合需要劫持读写流程的复杂需求。
五、总结
| 概念 | 作用 | 典型场景 |
|---|---|---|
toRefs / toRef | 解决解构丢失响应,保持 reactive 属性连接 | 组合式函数返回、解构赋值 |
shallowReactive | 只代理第一层,深层不响应 | 性能优化,大对象且无需深层响应 |
shallowRef | 只追踪 .value 变化,内部对象无响应 | 存储第三方实例、大对象 |
readonly | 递归只读代理,防止修改 | 保护 props、对外暴露不可变数据 |
shallowReadonly | 第一层只读,深层可改 | 结构很深但只需保护顶层 |
customRef | 完全自定义 ref 的 get/set 行为 | 防抖、节流、本地存储同步、联动、校验 |
理解这些进阶 API,能让你在复杂场景下既保证响应式的正确性,又能灵活优化性能,构建更健壮的 Vue 应用。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Vue:toRef / toRefs | 保持解构响应性 |
| Vue:shallowRef | 浅响应 |
| Vue:readonly | 只读代理 |
| Vue:customRef | 自定义 ref |
相关文章
依赖收集与派发更新机制
说明 Vue 3 中 track / trigger 与 targetMap 的工作方式。前置:v2-v3 差异。
Vue 2 与 Vue 3 响应式差异
Vue 2 与 Vue 3 响应式实现的核心差异如下。基础概念见 Vue 基础。
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
状态管理:Pinia 与 Vuex
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础。
Vue 生态关键库深度集成
VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。
测试策略
Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础。
Series
reactivity
1 / 3