人人都会AI编程

响应式系统的底层实现原理

更新时间:2026-07-11

响应式(Reactivity)是现代前端框架的灵魂能力之一:当数据发生变化时,依赖这些数据的界面会自动更新,开发者无需手动操作 DOM。在 Vue 和 MobX 等框架中,响应式系统的底层实现已经从 Object.defineProperty 全面演进到了 ProxyReflect。本节将揭开这一机制的面纱,帮助你理解数据劫持、依赖收集与触发更新是如何闭环运作的。

核心三步:劫持 → 收集 → 触发

一个完整的响应式系统包含三个紧密衔接的环节:

  1. 数据劫持:拦截对目标对象属性的访问与修改,让任何读写操作都“被监视”。
  2. 依赖收集:当副作用函数(如渲染函数、计算属性)读取被劫持的属性时,记录下“谁在用这个数据”。
  3. 触发更新:当被劫持的属性值发生变化时,通知所有依赖该数据的副作用函数重新执行。

用一句话总结:访问时收集,修改时通知。接下来我们看如何借助 ES6 的 Proxy 与 Reflect 将这三个环节串联成一个简洁而强大的系统。

使用 Proxy 实现数据劫持

Proxy 可以创建一个对象的代理,拦截对该对象的 13 种底层操作,包括属性读取(get)、属性设置(set)、属性删除(deleteProperty)、in 操作符检查(has)等。这与 Object.defineProperty 只能拦截已有属性的 getter/setter 有本质区别。

结合 Reflect,可以确保拦截后仍能正确触发对象的默认行为(比如保持原型链和属性描述符的正确性)。下面是一个基础的结构:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 1. 调用 Reflect.get 保证默认行为
      const result = Reflect.get(target, key, receiver);
      // 2. 收集依赖(track)
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      // 只有值真的发生了变化才触发更新
      if (oldValue !== value) {
        // 3. 触发更新(trigger)
        trigger(target, key);
      }
      return result;
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey) {
        trigger(target, key);
      }
      return result;
    }
  });
}

依赖收集(track)

当副作用函数正在执行时,它会访问响应式对象的某些属性。我们需要在此期间记录“哪个副作用函数依赖于哪个对象的哪个属性”。为此,可以维护一个全局的数据结构(通常是 WeakMap<target, Map<key, Set<effect>>>),利用 WeakMap 的弱引用特性自动回收不再使用的对象。

  • 需要一个全局变量(如 activeEffect)标记当前正在执行的副作用函数。
  • get 拦截器中,将 activeEffect 注册到目标对象→属性→副作用集合中。
const targetMap = new WeakMap();
let activeEffect = null;

function track(target, key) {
  if (!activeEffect) return; // 当前没有在执行的副作用,无需收集
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
}

触发更新(trigger)

当通过 setdeleteProperty 修改了对象的属性时,需要找到所有依赖于该属性的副作用函数并重新执行它们。

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const deps = depsMap.get(key);
  if (deps) {
    // 使用新的 Set 复制一份再遍历,避免在执行过程中增删导致死循环
    const effectsToRun = new Set(deps);
    effectsToRun.forEach(effect => effect());
  }
}

副作用函数与 effect 包装器

为了让依赖收集能够感知当前的副作用,需要一个 effect 函数来包裹副作用函数,并将其赋值给全局的 activeEffect,执行完毕后恢复。

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    fn();          // 执行期间会触发 get → track
    activeEffect = null;
  };
  effectFn();      // 立即执行以收集初始依赖
}

在此基础上可以构建 computedwatch 等更高级的响应式 API,它们本质上都是基于相同的 track/trigger 机制。

一个完整的迷你响应式系统示例

const state = reactive({ count: 0, text: 'hello' });

effect(() => {
  console.log('count is:', state.count);
});
// 立即输出: count is: 0

state.count = 1; // 输出: count is: 1
state.count = 2; // 输出: count is: 2
state.text = 'world'; // 没有依赖 text 的副作用,不会触发额外输出

数据变更自动引发了副作用的重执行,整个过程对开发者完全透明。

与 Object.defineProperty 的对比优势

在 ES6 之前,Vue 2 通过 Object.defineProperty 递归遍历对象的每个属性,手动添加 getter/setter。这种方案存在几大痛点,而 Proxy 几乎全部解决:

| 对比维度 | Object.defineProperty | Proxy |
|---------|----------------------|-------|
| 拦截范围 | 只能劫持已存在的属性,新增/删除属性无法检测(需通过 Vue.set/delete) | 可拦截 13 种操作,属性增删改查全覆盖 |
| 数组处理 | 需要重写数组的 7 个变异方法(push、pop 等)才能监听变化 | 原生即可监听数组索引赋值、length 修改等 |
| 初始化性能 | 代理整个对象,递归深度遍历,初始化成本高 | 代理在访问时按需进行(懒代理),性能更优 |
| 代码复杂度 | 需要特殊处理数组、新增属性逻辑,内部代码分散 | 统一通过 handler 拦截,逻辑集中,可维护性更好 |
| 属性描述符 | 会把数据属性转为访问器属性,原始属性描述丢失 | 通过 Reflect 保持原始属性特性,更透明 |

真实世界中的应用

Vue 3 的响应式系统正是基于 ProxyReflect 重写,彻底解决了 Vue 2 中因无法检测对象增删而导致的诸多“兼容性注意”和额外的 API 学习成本。同时,得益于 Proxy 的懒代理特性,深层对象只有在被访问到时才会被递归代理,显著提升了大型响应式数据的初始化速度。理解这套原理,不仅让你看懂框架源码更加轻松,也为面试中深入解释“Vue 3 为什么更快”提供了有力支撑。

响应式系统的底层实现,本质上是 “观察者模式”与“拦截器模式”的一次优雅联姻。掌握了它,你就在前端核心机制的理解上又迈进了一大步。