Vue 3 响应式内核完全解密:reactive & effect 与 Vue 2 Watcher 史诗对决

4 阅读4分钟

⚡ Vue 3 响应式原理终极指南:reactive/effect 与 Vue 2 Watcher 全面对决

✍️ 作者:皮皮大人

响应式是 Vue 的灵魂引擎,从 Vue 2 基于 Object.defineProperty 的 Watcher 模式,到 Vue 3 基于 Proxy 的 reactive + effect 架构,不仅是底层 API 的替换,更是一场关于性能、可读性与工程设计的革命。


📖 目录结构

🔍 深度导航

  • 1. 响应式哲学与双版本鸟瞰
  • 2. Vue 2 响应式内幕:Watcher、Dep、defineProperty 完全解剖
  • 3. 痛点总结:Vue 2 的七宗罪
  • 4. Vue 3 响应式基石:Proxy 与 Reflect 超能力
  • 5. reactive API 实现原理 + 手写极简 reactive (带 console 埋点)
  • 6. effect 副作用系统:依赖收集与触发 (track/trigger)
  • 7. 进阶:嵌套 effect、effect 栈、调度执行 (scheduler) 与 stop
  • 8. 重磅对比:reactive vs ref vs Vue 2 的 data/computed/watch
  • 9. 实战 demo 集锦:数组、Map、Set、深层对象响应式差异
  • 10. 性能对决与 Vue 3 优化策略
  • 11. 手写完整版 mini-reactive 内核(完整可运行代码)
  • 12. 总结:从 Watcher 到 Effect 的心智革命

1. 响应式哲学与双版本

响应式系统的终极目标:当状态变更,所有依赖该状态的地方自动更新。Vue 2 通过劫持对象属性 + 发布订阅实现;Vue 3 则利用 ES6 Proxy 代理整个对象,并引入 effect 作为通用副作用机制。

核心差异对比表:

特性Vue 2Vue 3
底层代理Object.definePropertyProxy + Reflect
依赖收集粒度属性级 Dep + Watcher属性级 + 副作用级精确映射
数组/新增属性需重写数组方法 / Vue.set原生支持,完美拦截
初始化性能递归遍历所有属性,重懒代理,按需递归,性能大幅提升
副作用 APIWatcher, $watch, computedeffect, watch, watchEffect
代码抽象类风格 (Watcher, Dep)函数式 + 闭包,更灵活

从 Vue 3 开始,响应式甚至可以独立于框架使用,这是质的飞跃。


2. Vue 2 响应式内幕:Watcher、Dep、defineProperty 完全解剖

Vue 2 中,当 data 被传入时,Observer 会递归遍历每个属性,并使用 defineReactive 将其转换为 getter/setter。每个属性会持有一个 Dep 实例,Dep 中存放 Watcher。Watcher 可能是渲染 watcher、计算属性 watcher 或用户自定义 watch。

2.1 核心简化实现 (带控制台日志)

// 👇 打开控制台运行这段代码,观察Vue2风格响应式
let uid = 0;
class Dep {
  constructor() {
    this.id = uid++;
    this.subs = new Set();
    console.log(`[Dep created] id: ${this.id}`);
  }
  depend() {
    if (Dep.target) {
      this.subs.add(Dep.target);
      console.log(`[Dep.${this.id}] 收集 Watcher`);
    }
  }
  notify() {
    console.log(`[Dep.${this.id}] 派发更新, watcher数量: ${this.subs.size}`);
    this.subs.forEach(watcher => watcher.update());
  }
}
Dep.target = null;

class Watcher {
  constructor(getter, callback) {
    this.getter = getter;
    this.callback = callback;
    this.value = this.get();
  }
  get() {
    Dep.target = this;
    let value = this.getter();
    Dep.target = null;
    return value;
  }
  update() {
    const oldVal = this.value;
    const newVal = this.getter();
    if (newVal !== oldVal) {
      this.callback(newVal, oldVal);
      this.value = newVal;
    }
    console.log(`[Watcher] update executed, newVal=${newVal}`);
  }
}

function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        dep.notify();
      }
    }
  });
}

// 使用示例
const data = { count: 0 };
defineReactive(data, 'count', 0);
new Watcher(() => data.count, (newVal, oldVal) => {
  console.log(`✨ 视图更新: count from ${oldVal} to ${newVal}`);
});
data.count = 10; // 控制台可见完整依赖收集 & 派发流程

2.2 Vue 2 数组监听缺陷与变通

由于 defineProperty 无法拦截数组索引变动,Vue 2 重写了数组的 7 个变异方法(push/pop/shift/unshift/splice/sort/reverse),而对于直接通过索引修改 arr[0]=xx 无感知,必须使用 $set

// Vue 2 数组痛点示例
const arr = [1,2,3];
// 通过索引修改不能触发更新 ❌
arr[0] = 99;
// 必须使用 Vue.set
Vue.set(arr, 0, 99);

3. 痛点总结:Vue 2 的七宗罪

  1. 无法检测属性的添加/删除 → 需 Vue.set/Vue.delete
  2. 数组索引和 length 变更受限 → 变异方法 hack
  3. 初始化递归性能开销大
  4. 不支持 Map、Set、WeakMap 等数据结构
  5. Watcher 重复收集偶尔冗余
  6. 虚拟 DOM 更新与响应式耦合较深
  7. TypeScript 类型推导不完美

4. Vue 3 响应式基石:Proxy 与 Reflect 超能力

Proxy 可以代理整个对象,拦截 get/set/deleteProperty 等操作,完全解决 Vue 2 的痛点。配合 Reflect 保证 this 指向正确。

// 展示 Proxy 核心能力
const target = { name: '皮皮', tags: ['vue','react'] };
const handler = {
  get(obj, prop, receiver) {
    console.log(`[Proxy get] 读取 ${String(prop)}`);
    return Reflect.get(obj, prop, receiver);
  },
  set(obj, prop, value, receiver) {
    console.log(`[Proxy set] ${String(prop)} = ${value}`);
    return Reflect.set(obj, prop, value, receiver);
  },
  deleteProperty(obj, prop) {
    console.log(`[Proxy delete] 删除 ${String(prop)}`);
    return Reflect.deleteProperty(obj, prop);
  }
};
const proxyData = new Proxy(target, handler);
proxyData.newProp = '动态新增';   // 拦截成功 ✅
delete proxyData.tags;

Vue 3 正是利用这样的全能拦截,实现了 reactive 函数。


5. reactive API 实现原理 + 手写极简 reactive (带 console 埋点)

Vue 3 的 reactive 基于 Proxy,并且使用了懒递归:只有当读取的属性值是对象时,才会递归调用 reactive 进行代理。

// 简易reactive (带日志版本)
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 dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  dep.add(activeEffect);
  console.log(`[track] 依赖收集: ${key}, effect: ${activeEffect.name || 'anonymous'}`);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    console.log(`[trigger] 触发更新: ${key}, 待执行副作用数量: ${dep.size}`);
    dep.forEach(effectFn => effectFn());
  }
}

function reactive(raw) {
  return new Proxy(raw, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      // 懒递归: 如果值是对象且不是null, 则递归代理
      if (res && typeof res === 'object') {
        return reactive(res);
      }
      return res;
    },
    set(target, key, value, receiver) {
      const oldVal = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldVal !== value) {
        trigger(target, key);
      }
      return result;
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) trigger(target, key);
      return result;
    }
  });
}

function effect(fn) {
  const effectFn = () => {
    try {
      activeEffect = effectFn;
      fn();
    } finally {
      activeEffect = null;
    }
  };
  effectFn(); // 立即执行,触发收集
  return effectFn;
}

// demo
const state = reactive({ count: 0, deep: { msg: 'hello' } });
effect(() => {
  console.log(`🎯 effect执行: state.count = ${state.count}`);
});
state.count++;   // 控制台中会显示 track + trigger 完整流程
state.deep.msg = 'world'; // 深层也响应

6. effect 副作用系统:依赖收集与触发 (track/trigger)

effect 相当于 Vue 2 中的 Watcher,但更纯粹 —— 任何响应式数据的变化都会重新执行 effect。依赖收集的核心三要素:

  • targetMap (WeakMap) : 对象 → depsMap
  • depsMap (Map) : 属性 → dep (Set)
  • dep : 存储当前活跃 effect 的集合

每次 effect 执行时会设置 activeEffect,在 reactive 的 get 中调用 track;set 时调用 trigger。


7. 进阶:嵌套 effect、effect 栈、调度执行 (scheduler) 与 stop

7.1 嵌套 effect

Vue 3 中 effect 可以嵌套,内部 effect 不会干扰外部依赖收集,内部依赖收集完毕恢复 activeEffect 为外层。实现依赖 effectStack。

// 伪代码展示嵌套机制
const effectStack = [];
function effect(fn) {
  const runner = () => {
    try {
      effectStack.push(runner);
      activeEffect = runner;
      fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  };
  runner();
  return runner;
}

7.2 调度执行 scheduler

effect(() => {
  console.log(state.count);
}, {
  scheduler(fn) {
    // 自定义调度,比如异步更新或防抖
    setTimeout(fn, 100);
  }
});

7.3 停止响应 (stop)

在 Vue 3 中,effect 返回 runner 并挂载一个 stop 方法可以清除所有依赖。

const runner = effect(() => { /* ... */ });
runner.effect.stop(); // 停止响应

8. 重磅对比:reactive vs ref vs Vue 2 的 data/computed/watch

API / 概念Vue 2Vue 3
声明响应式状态data 函数返回对象reactive / ref
基本类型包装直接放在 data 中ref 包裹,模板自动解包
计算属性computed 函数computed (接受 getter/setter)
监听器watch, $watchwatch, watchEffect
响应式丢失不易丢失需使用 toRefs 解构 reactive

ref 实现核心{ value: ... } 并且同样被 reactive 代理转换。


9. 实战 demo 集锦:数组、Map、Set、深层对象响应式差异

下面展示 Vue 3 相比 Vue 2 的巨大优势。

// Vue 3 中数组索引与长度完美代理
const arrState = reactive([1,2,3]);
effect(() => console.log('arr len', arrState.length, 'arr[0]', arrState[0]));
arrState[0] = 100;   // ✅ 触发
arrState.length = 0;  // ✅ 触发

// Map/Set 集合响应式
const mapState = reactive(new Map([['key','val']]));
effect(() => console.log('map key:', mapState.get('key')));
mapState.set('key', 'newVal'); // ✅ 响应

10. 性能对决与 Vue 3 优化策略

  • 初始化优化:懒代理,只有访问属性时才递归,初始化速度提升 2~3 倍。
  • 内存优化:WeakMap 无侵入式存储依赖,不会造成内存泄漏。
  • 更精确的更新:effect 和依赖一一对应,避免 watcher 多次重复计算。
  • 编译优化:静态提升和 PatchFlags 减少 diff 压力。

11. 手写完整版 mini-reactive 内核(完整可运行代码)

整合前面所有思路,提供一个可直接复制的迷你响应式库,支持 reactive/effect/stop/scheduler (简化演示核心):

// mini-reactive-full.js
let activeEffect = null;
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  dep.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effectFn => effectFn());
  }
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      if (res && typeof res === 'object') return reactive(res);
      return res;
    },
    set(target, key, value, receiver) {
      const oldVal = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldVal !== value) trigger(target, key);
      return result;
    },
    deleteProperty(target, key) {
      const had = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (had && result) trigger(target, key);
      return result;
    }
  });
}

function effect(fn, options = {}) {
  const effectFn = () => {
    try {
      activeEffect = effectFn;
      return fn();
    } finally {
      activeEffect = null;
    }
  };
  effectFn();
  if (options.scheduler) effectFn.scheduler = options.scheduler;
  return effectFn;
}

// 测试代码:打开控制台复制运行
const user = reactive({ name: '皮皮', age: 30 });
effect(() => console.log(`姓名: ${user.name}, 年龄: ${user.age}`));
user.name = '皮皮大人';  // 立刻重绘
user.age = 31;

12. 总结:从 Watcher 到 Effect 的心智革命

Vue 3 的响应式系统彻底挣脱了 defineProperty 的桎梏,带来了完整的数据侦测能力、极佳的性能以及更优雅的 Composition API。effect 函数的简洁与灵活,让开发者可以更低成本地构建复杂响应式逻辑。

学完本文你应该收获

  • ✅ 理解 Vue 2 响应式的底层实现与局限
  • ✅ 掌握 Proxy/Reflect 在 reactive 中的核心用法
  • ✅ 能够手写一个完整的迷你版 reactive & effect
  • ✅ 精通 track/trigger 依赖收集闭环
  • ✅ 明白为何 Vue 3 在数组、新增属性上如此丝滑

皮皮大人寄语:源码之路没有捷径,但本文提供的所有代码段都可以直接在你的控制台运行,反复调试,直到融会贯通。欢迎收藏本文,面试时作为杀手锏。


📌 附录:扩展学习资料

  • Vue 3 源码: packages/reactivity

  • 《Vue.js 设计与实现》

  • MDN Proxy / Reflect