FORMA

响应式进阶

toRefsshallowRefreadonlycustomRef 等 API 的适用场景与注意点。前置:依赖收集

以下是 Vue 3 响应式系统的高级主题详解,涵盖响应式丢失的成因与补救、浅响应式、只读响应式及自定义响应式。

一、响应式丢失问题

1. 成因:解构 reactive 对象

reactive 返回的是一个 Proxy 对象,其属性访问会被拦截。但若将该对象的属性解构到普通变量,这些变量就只是初始值的快照,不再具有响应性。

js
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. 解决方案:toRefstoRef

toRefs

reactive 对象转换为一个普通对象,该对象的每个属性都是指向原对象对应属性的 ref。这样解构后得到的每个属性都是 ref 对象,保持响应连接。

js
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 的区别是只处理一个属性。

js
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 直接操作源对象的对应属性,并在读写时触发依赖收集与更新。

js
function toRef(target, key) {
  return {
    get value() {
      return target[key];
    },
    set value(newVal) {
      target[key] = newVal;
    },
  };
}

注意:toRef 即使源属性当前不存在,也会创建一个可写的 ref,当设置值时会在源对象上新增属性(响应式)。

二、浅响应式(shallowRef / shallowReactive

1. shallowReactive

只拦截对象的第一层属性,深层属性不会被转换为响应式。适用于大型数据结构,但只需监听顶层变化。

js
import { shallowReactive } from "vue";

const state = shallowReactive({
  user: { name: "Alice" }, // 深层对象,不是响应式的
  count: 0, // 第一层,响应式
});

state.count++; // ✅ 触发更新
state.user.name = "Bob"; // ❌ 不会触发更新,因为 user 不是响应式

性能收益:避免深度递归代理,提升初始化和访问速度,节省内存。 风险:修改深层对象时不会触发视图更新,容易造成 bug。因此仅适用于结构扁平、深层属性不会变更的场景(如静态配置)。

2. shallowRef

只对 .value 的访问进行响应式追踪,如果 .value 是一个对象,不会递归将其内部属性转为响应式

js
import { shallowRef } from "vue";

const data = shallowRef({ count: 0 });
data.value.count = 1; // ❌ 不会触发更新
data.value = { count: 1 }; // ✅ 替换整个 value 会触发更新

典型应用:存储大对象但不需要其内部属性的响应式(如图表配置、第三方实例)。配合 triggerRef 可手动强制更新。

3. 何时使用浅响应式?

  • 性能敏感的大型列表/树,仅需监听引用变化。
  • 第三方库对象(如 ECharts 实例)不希望被代理。
  • markRaw 标记永久不可代理的对象配合。

三、只读响应式(readonlyshallowReadonly

1. readonly

接受一个响应式对象(或普通对象),返回一个只读代理。任何修改操作(属性赋值、删除)都会在开发模式下抛出警告。

js
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

只确保对象的第一层属性只读,深层属性仍可修改。

js
const state = shallowReadonly({ user: { name: "Alice" }, version: 1 });
state.version = 2; // 警告
state.user.name = "Bob"; // ✅ 允许(深层不是只读)

应用场景:深层结构巨大但只需保护顶层引用的场合。

3. 嵌套只读

readonly 会递归地将所有嵌套对象也变为只读,而 shallowReadonly 只影响第一层。

四、自定义响应式(定制 ref

通过 customRef 可以创建带有自定义逻辑的 ref,例如实现防抖、节流、本地存储同步等。

1. customRef 基本用法

js
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. 使用示例

js
const text = useDebouncedRef("hello", 500);
// 在模板中绑定 input,输入停止 500ms 后才会更新 text

3. 更多应用场景

  • 本地存储同步 refget 时从 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

Series

reactivity

1 / 3