Skip to content

在 Vue 3 的响应式系统中,reactive 是构建深层响应式对象的核心 API。其背后依赖于 ES6 的 Proxy 特性,通过拦截对象的基本操作来建立响应式链条。要真正掌握 reactive,必须深入理解它究竟拦截了哪些操作,以及这些拦截如何协同工作以实现自动依赖追踪和更新触发。本节将从源码层面剖析 reactive 的本质,详细解析 getsethasownKeys 等拦截器的作用机制。

一、Proxy 的核心作用:操作拦截

reactive 的实现基于 Proxy。当你调用 reactive(obj) 时,Vue 会返回一个 Proxy 实例,该实例代理了原始对象 objProxy 可以拦截对目标对象的各种操作,如属性读取、赋值、枚举等。

reactive 使用的 Proxy handler 定义了多个拦截器,每个拦截器负责处理特定的操作,并在适当时机调用 track(依赖收集)或 trigger(触发更新)。

二、get 拦截器:依赖收集的起点

get 拦截器是响应式系统中最关键的一环,它负责在属性读取时进行依赖收集。

ts
const get = (target, key, receiver) => {
  // 1. 收集依赖
  track(target, key);

  // 2. 获取原始值
  const result = Reflect.get(target, key, receiver);

  // 3. 深层响应式:如果值是对象,递归 reactive
  if (isObject(result)) {
    return reactive(result);
  }

  // 4. 返回结果
  return result;
};

详细解析:

  1. track(target, key): 这是依赖收集的核心。track 函数会检查当前是否有正在运行的副作用(如组件渲染、watchEffect)。如果有,它会将该副作用记录为 target 对象上 key 属性的依赖。这样,当 key 被修改时,Vue 就知道需要重新执行哪些副作用。

  2. Reflect.get: 使用 Reflect.get 安全地获取目标对象的属性值。receiver 参数确保 this 绑定正确,特别是在访问 getter 属性时。

  3. 深层响应式: 如果获取的值是一个对象(或数组),reactive 会递归地将其转换为响应式对象。这是 reactive 实现“深层响应式”的关键。它确保了无论数据嵌套多深,修改任何层级的属性都能被追踪。

  4. 返回值: 最终返回经过响应式处理的值。对于原始值(number、string 等),直接返回;对于对象,则返回其 reactive 代理。

三、set 拦截器:更新触发的枢纽

set 拦截器负责在属性被修改时触发更新。

ts
const set = (target, key, value, receiver) => {
  // 1. 获取旧值
  const oldValue = target[key];

  // 2. 设置新值
  const result = Reflect.set(target, key, value, receiver);

  // 3. 检查值是否真正改变
  if (!Object.is(value, oldValue)) {
    // 4. 触发依赖更新
    trigger(target, key);
  }

  // 5. 返回结果
  return result;
};

详细解析:

  1. 获取旧值: 在设置新值前,先读取旧值,用于后续的变更检测。

  2. Reflect.set: 安全地设置属性值。与 get 一样,receiver 确保 this 正确绑定。

  3. 变更检测: 使用 Object.is 比较新旧值。Object.is 能正确处理 NaN+0/-0 的比较,比 === 更精确。只有当值真正改变时,才触发更新,避免不必要的渲染。

  4. trigger(target, key): 这是更新触发的核心。trigger 函数会查找 target 对象上 key 属性的所有依赖(即之前通过 track 收集的副作用),并将它们加入调度队列,等待执行。

四、has 拦截器:in 操作符的响应式支持

has 拦截器用于拦截 in 操作符和 with 语句中的属性检查。

ts
const has = (target, key) => {
  // 收集依赖:检查属性是否存在也是一种依赖
  track(target, key);
  // 返回属性是否存在
  return Reflect.has(target, key);
};

作用场景:

js
const state = reactive({ count: 0 });

// 模板中使用 v-if="count in state"
if ('count' in state) {
  // 会触发 has 拦截器,收集依赖
}

state.count 被添加或删除时,依赖于 'count' in state 的副作用需要重新执行。has 拦截器通过 track 收集这些依赖,确保 in 操作的响应式。

五、ownKeys 拦截器:键枚举的响应式

ownKeys 拦截器用于拦截 Object.keysObject.getOwnPropertyNamesObject.getOwnPropertySymbolsfor...in 循环等操作。

ts
const ownKeys = (target) => {
  // 收集依赖:枚举对象的所有键也是一种依赖
  track(target, ITERATE_KEY);
  // 返回所有自有属性键
  return Reflect.ownKeys(target);
};

详细解析:

  1. ITERATE_KEY: 这是一个特殊的 key(通常为 Symbol('iterate'))。当枚举操作发生时,track 收集的依赖是针对 ITERATE_KEY 的。这样,当对象的任何键被添加或删除时,trigger 可以通过 ITERATE_KEY 触发所有依赖于枚举操作的副作用。

  2. 触发场景

    js
    // 以下操作都会触发 ownKeys
    Object.keys(state);
    for (let key in state) { /* ... */ }

    如果 state 后续通过 delete state.countstate.newKey = 1 修改,依赖于 Object.keys(state) 的计算属性或模板需要重新执行。

六、其他重要拦截器

除了上述四个,reactive 还实现了其他拦截器以确保完整性和正确性。

1. deleteProperty

拦截 delete 操作符。

ts
const deleteProperty = (target, key) => {
  const result = Reflect.deleteProperty(target, key);
  if (result) {
    // 属性删除成功,触发更新
    trigger(target, key, 'delete');
  }
  return result;
};

2. defineProperty

拦截 Object.defineProperty

ts
const defineProperty = (target, key, descriptor) => {
  const result = Reflect.defineProperty(target, key, descriptor);
  if (result) {
    // 属性描述符改变,触发更新
    trigger(target, key);
  }
  return result;
};

3. getOwnPropertyDescriptor

拦截 Object.getOwnPropertyDescriptor

ts
const getOwnPropertyDescriptor = (target, key) => {
  // 访问属性描述符也是一种依赖
  track(target, key);
  return Reflect.getOwnPropertyDescriptor(target, key);
};

七、响应式链条的构建过程

当一个对象被 reactive 包装后,完整的响应式链条如下:

  1. 读取属性state.count):

    • 触发 get 拦截器。
    • 执行 track(state, 'count'),收集当前副作用作为依赖。
    • 返回值(若为对象,递归 reactive)。
  2. 修改属性state.count = 1):

    • 触发 set 拦截器。
    • 比较新旧值,若不同则执行 trigger(state, 'count')
    • trigger 查找 statecount 的所有依赖,并调度执行。
  3. 枚举属性for (let key in state)):

    • 触发 ownKeys 拦截器。
    • 执行 track(state, ITERATE_KEY),收集依赖。
    • state 的键集合改变时,通过 ITERATE_KEY 触发更新。
  4. 检查属性'count' in state):

    • 触发 has 拦截器。
    • 执行 track(state, 'count'),收集依赖。
    • count 属性被添加或删除时,触发更新。

八、设计哲学:透明的代理

reactive 的设计目标是“透明”。它通过 Proxy 拦截所有可能改变或读取对象状态的操作,使得响应式行为对使用者透明。开发者可以像操作普通对象一样操作 reactive 对象,而 Vue 在背后自动建立依赖关系。

这种设计的优势是开发体验极佳,但代价是:

  • 无法代理 MapSetWeakMapWeakSet 等集合类型(它们有自己的一套方法,需特殊处理)。
  • 对某些边缘操作(如 __proto__caller)的处理需要额外注意。

九、总结

reactive 的本质是通过 Proxy 拦截对象的底层操作,构建一个完整的响应式链条:

  • get:读取属性时收集依赖。
  • set:设置属性时触发更新(仅当值改变)。
  • hasin 操作时收集依赖。
  • ownKeys:枚举操作时收集依赖(使用 ITERATE_KEY)。

这些拦截器协同工作,确保了 Vue 能够精确地追踪数据依赖,并在数据变化时高效地触发视图更新。理解这些底层机制,是掌握 Vue 3 响应式系统精髓的关键。