Skip to content

✨ 响应式数据的本质 👌

要点速览

  • “响应式”本质是“对数据操作的拦截”,在拦截里执行我们的业务逻辑(依赖收集、调度更新等)。
  • ref 用“访问器属性”的 get/set 拦截对 value 的读写;reactiveProxy 拦截对象成员的读写、枚举、删除、原型等操作。
  • 是否会拦截的核心判断:操作是否发生在“被拦截对象的成员上”。非成员操作(如变量重新赋值)不会触发拦截。
  • 同一数据上的不同操作触发的是“不同的拦截逻辑”,不要期待一个统一拦截能搞定全部行为。

学习目标

  • 看懂 refreactive 的拦截点与触发条件。
  • 会判断某个操作是否“发生在成员上”,从而是否会触发拦截逻辑。
  • 能区分:在 ref 包裹对象上改“对象成员”,这是 reactive 的拦截,不是 ref 的拦截。

什么是响应式数据?

  • 能拦截我们对数据的操作(读、写、删、枚举等),并在拦截逻辑里执行我们写好的处理,即可称为“响应式数据”。
  • Vue 以 refreactive 两条路径产出“可拦截的对象”,从而具备响应能力。

为什么要学会判断“是否会拦截”?

  • 拦截逻辑是分散在不同操作上的,不是单一入口;不同操作触发不同拦截代码。
  • 并非所有操作都能拦截:例如变量重新赋值、对不可拦截类型的“成员操作”等。
  • 学会判断能避免“以为会更新、结果没触发”的尴尬,用正确方式驱动更新。

ref 的拦截原理(访问器属性)

这是一个简化版的 ref 实现思路:通过“访问器成员 value 的 get 与 set”拦截对 value 的读写。

js
class RefImpl {
  constructor(value) {
    // 简化:对象则交给 reactive 处理;原始值直接保存
    this._value =
      typeof value === "object" && value !== null ? reactive(value) : value;
    this.__v_isRef = true;
  }
  get value() {
    console.log("getter value", this._value);
    // 读取时可进行依赖收集(此处演示略)
    return this._value;
  }
  set value(newVal) {
    console.log("setter newVal", newVal);
    // 写入时可做变更判断与派发更新(此处演示略)
    this._value = newVal;
  }
}

function ref(value) {
  return new RefImpl(value);
}

结论

  • ref 的拦截发生在对“refObj.value 的读写”这一成员操作上。
  • 写入时可做“变更判断”以避免不必要的更新;读取时可做“依赖收集”。

ref:是否触发拦截的演示

js
// 创建拦截对象
let state = ref({ name: "zhangsan" });

// 变量重新赋值:不是成员操作,不触发 ref 的拦截
state = { name: "lisi" };

// 成员写入:发生在 ref 对象的成员上,触发拦截
state.value = { name: "lisi" };

// 对 value(对象)的成员写入:这是 reactive 的拦截,不是 ref 的拦截
state.value.name = "wangwu";

// 写入字符串:只触发 ref 的 set;字符串的“成员拦截”不存在
state.value = "abc";

reactive 的拦截原理(Proxy)

使用 Proxy 对对象进行拦截,常见是 getset 两个成员操作的入口:

js
const state = { name: "Bob" };

const proxyCache = new WeakMap();

function reactive(target) {
  if (typeof target !== "object" || target === null) {
    return target;
  }

  if (proxyCache.has(target)) {
    return proxyCache.get(target);
  }

  const handler = {
    // 读取成员拦截
    get(obj, key, receiver) {
      const v = Reflect.get(obj, key, receiver);
      console.log("get 拦截逻辑", key, v);
      return typeof v === "object" && v !== null ? reactive(v) : v;
    },
    // 写入成员拦截
    set(obj, key, val, receiver) {
      const prev = Reflect.get(obj, key, receiver);
      const ok = Reflect.set(obj, key, val, receiver);
      if (ok && prev !== val) {
        console.log("set 拦截逻辑成功");
      }
      return ok;
    },
    deleteProperty(obj, key) {
      const ok = Reflect.deleteProperty(obj, key);
      if (ok) {
        console.log("delete 拦截逻辑成功");
      }
      return ok;
    },
    // ...
  };

  const proxy = new Proxy(target, handler);
  proxyCache.set(target, proxy);
  return proxy;
}

const proxyState = reactive(state);

console.log(proxyState.name); // 触发 get
proxyState.name = "Alice"; // 触发 set

进一步拦截点

  • get/set 外,Vue 在 reactive 中还会拦截:hasdeletePropertyownKeysgetOwnPropertyDescriptor 等,以保证与原对象的行为一致并维护不变式。
  • 枚举相关建议使用 Reflect.ownKeys 保持 string 与 Symbol 键的一致性。

如何判断“是否会拦截”?

核心标准:操作是否发生在“拦截对象的成员上”。下面用一组示例来直观判断:

jsx
// 示例 1:对象成员读写 → 触发 reactive 的 get/set
const proxy = new Proxy({ name: 'Ak' }, {
  get(t, k) { console.log('get', k); return t[k] },
  set(t, k, v) { console.log('set', k, v); t[k] = v; return true }
})
console.log(proxy.name)   // get
proxy.name = 'Sky'        // set

// 示例 2:ref 的成员写入 → 触发 ref 的 set
const count = ref(0)
count.value = 1

// 示例 3:ref 包裹对象成员写入 → 触发 reactive 的 set(不是 ref 的拦截)
const user = ref({ name: 'A' })
user.value.name = 'B'

// 示例 4:字符串成员不可拦截 → 仅触发 ref 的 set
const title = ref('hello')
title.value = 'world'

// 示例 5:数组操作(push)→ 内部会触发若干 set(索引与 length)
const list = new Proxy([], {
  set(t, k, v) { console.log('array set', k, v); t[k] = v; return true }
})
list.push(4)

学会判断拦截

  • 变量重新赋值不是成员操作,不会触发拦截。
  • ref 包裹的对象上改“对象成员”,触发的是 reactive 的拦截。
  • 基本类型的“成员操作”不可拦截(字符串、数字、布尔)。

常见坑位

易错点

  • 把“非成员操作”误认为会触发拦截,导致更新逻辑没有执行。
  • ref 包裹对象上做成员写入,却以为是 ref 的拦截;实际是 reactive 的拦截路径。
  • 忘记通过 state.value 访问 ref 的值,直接 state = ... 导致不会触发拦截。
  • 枚举对象键时用 Object.keys,忽略了 Symbol 键与不可枚举键;建议 Reflect.ownKeys(配合 Proxy 的 ownKeys )。
枚举对象键的区别
js
Object.keys() - 只枚举可枚举的字符串键,不包括 Symbol
Object.getOwnPropertyNames() - 枚举所有字符串键(包括不可枚举),不包括 Symbol
Object.getOwnPropertySymbols() - 专门枚举 Symbol 键
Reflect.ownKeys() - 枚举所有键(字符串 + Symbol)

使用建议

  • 优先用 reactive 管对象/数组的成员操作,用 ref 管标量或需要“引用语义”的场景。
  • 判断拦截从“是否发生在成员上”入手,再结合对象是否被代理、成员类型是否可拦截。
  • 与渲染更新配合时,建议使用“微任务合并 + 调度”的模式,避免多次同步更新导致抖动。

小结

  • 响应式的本质是“拦截数据操作并在拦截里执行处理”。
  • refreactive 的拦截点不同:分别拦截 value 的读写与对象成员的读写/枚举等。
  • 是否会拦截,取决于操作是否发生在“拦截对象的成员上”。学会正确判断,才能稳定驱动更新与行为。