人人都会AI编程

4.2 Vue 3 响应式:Proxy + Reflect 实现

更新时间:2026-07-11

Vue 2 的响应式系统基于 Object.defineProperty 实现,它通过“数据劫持”来拦截对象属性的读取和修改,并在这些操作中完成“依赖收集”和“派发更新”。理解这一流程,是理解 Vue 响应式工作原理的基础。

第一步:数据劫持——让对象变得“可观测”

在 Vue 2 中,当你把一个普通对象传给组件的 data 选项时,Vue 会遍历这个对象的所有属性,使用 Object.defineProperty 为每个属性定义 getter 和 setter。这个过程被称为“数据劫持”。

function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  observe(val);
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集:如果有人正在读取这个属性,记录下来
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 如果新值是对象,也要递归劫持
      observe(newVal);
      // 派发更新:通知所有依赖这个属性的订阅者
    }
  });
}

这一步骤让普通的 JavaScript 对象变成了“可观测”的响应式对象。一旦有代码读取或修改这些属性,Vue 就能介入并完成后续的关键动作。

两个需要知道的限制

  1. 无法检测属性的新增和删除

因为 defineProperty 只能劫持对象已有的属性。如果你动态给对象添加一个新属性(如 vm.newProp = 'value'),这个属性不会被自动转换成响应式。这也是为什么 Vue 2 需要提供 Vue.set / this.$setVue.delete / this.$delete 的原因。

  1. 数组的特殊处理

Object.defineProperty 原本可以监听数组索引的变化,但直接监听数组的每个索引在高频操作下性能很差,而且无法感知数组长度的变化。所以 Vue 2 选择重写了数组的 7 个变异方法(pushpopshiftunshiftsplicesortreverse),在这些方法被调用时手动触发更新。同时,通过索引直接修改数组元素(arr[0] = newVal)和修改长度(arr.length = 0)依然是不响应的。

第二步:依赖收集——建立“谁用了谁”的关系网

当组件渲染或计算属性求值时,会读取响应式对象的属性,从而触发该属性的 getter。Vue 在 getter 中完成“依赖收集”。

具体过程:

  1. 每个组件实例都有一个对应的 Watcher(渲染 Watcher),它负责在组件数据变化时重新渲染视图。
  2. 读取属性时,Vue 会把当前正在执行的 Watcher 压入一个全局的“目标栈”Dep.target
  3. 属性对应的 getter 会创建一个 Dep(依赖管理器)对象,并将当前的 Watcher 添加到 Dep 的订阅者列表中。
  4. 于是,DepWatcher 之间形成了一种多对多的关系:一个属性可能被多个组件依赖(多个 Watcher 订阅同一个 Dep),一个组件也可能依赖多个属性(一个 Watcher 订阅多个 Dep)。
// 简化的依赖收集过程
class Dep {
  constructor() {
    this.subs = []; // 存放 Watcher
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this); // 把当前 Watcher 添加到 Dep
    }
  }
}

// 在 getter 中
get() {
  const dep = new Dep();
  if (Dep.target) {
    dep.depend(); // 收集依赖
  }
  return val;
}

正是这层依赖关系,让 Vue 能够精确地知道“哪些组件依赖了哪些数据”。当数据变化时,不需要全局扫描,只需通知对应 Dep 里的所有 Watcher 即可。

第三步:派发更新——精确地通知变化

当修改响应式属性时,会触发该属性的 setter。setter 内部会:

  1. 更新属性值(如果新值是对象则递归劫持)。
  2. 调用该属性对应的 Depnotify() 方法。
  3. notify() 方法会遍历 subs 列表中的每一个 Watcher,调用它们的 update() 方法。
  4. Watcherupdate() 并不会立刻执行渲染,而是将自身推入一个异步更新队列。Vue 在同一个事件循环中收集到的所有 Watcher 更新,会被合并并批量执行,从而避免不必要的重复计算和 DOM 操作。
// 简化的派发更新过程
class Dep {
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

// 在 setter 中
set(newVal) {
  val = newVal;
  dep.notify(); // 通知所有订阅者
}

整个流程的直观理解

可以想象一个微信聊天群的比喻:

  • 响应式对象:聊天群
  • Dep:群成员列表
  • Watcher:群里的某个成员(组件)
  • getter:成员说“我对某个话题感兴趣”(读属性),话题就把他拉到一个专门的讨论组(依赖收集)
  • setter:有人对话题发言(改属性),消息自动推送给该讨论组内所有感兴趣的人(派发更新)

整个过程不需要任何人挨个通知,数据变化会自动、精确地流向需要更新视图的组件。

Vue 3 的改进

Vue 2 这种基于 Object.defineProperty 的实现虽然能工作,但存在无法劫持新增/删除属性数组响应式需要特殊处理等原生缺陷。Vue 3 使用 Proxy 取代了 Object.defineProperty,可以直接代理整个对象,监听属性添加、删除、数组索引修改和 length 变化,无需任何额外 API。同时,依赖收集和派发更新的核心流程没有根本性改变,依然是 getter 时 track,setter 时 trigger,只是实现更加一致和高效。

这一整套“数据劫持 → 依赖收集 → 派发更新”的机制,是 Vue 响应式系统的灵魂,也是“数据驱动视图”得以运转的底层支柱。