深入理解 Vue 响应式系统:从 Vue 2 到 Vue 3 的演进之路

5 阅读5分钟

深入理解 Vue 响应式系统:从 Vue 2 到 Vue 3 的演进之路

摘要:响应式系统是 Vue.js 的核心特性之一。本文将带你从零开始实现一个完整的响应式系统,深入理解 Vue 2 和 Vue 3 的实现原理差异,并附上完整可运行的代码示例。


一、什么是响应式?

简单来说,响应式就是数据变化时,视图自动更新

// 理想中的响应式
let data = { message: 'Hello' };

// 当数据变化时,自动执行回调
data.message = 'World'; // 自动触发更新,无需手动调用任何方法

Vue 的魔法就在于此。那么,它是如何做到的呢?


二、Vue 2 响应式原理:Object.defineProperty

2.1 核心思想

Vue 2 使用 Object.defineProperty() 劫持所有属性的 getter 和 setter:

  • getter 时收集依赖:谁用到了这个属性,就把它记录下来
  • setter 时派发更新:属性变化时,通知所有依赖进行更新

2.2 从零实现 Vue 2 响应式

第一步:定义 Dep(依赖收集器)
/**
 * Dep - Dependency 依赖收集器
 * 每个响应式属性都有一个 dep,用于存储所有依赖这个属性的 watcher
 */
class Dep {
  static target = null; // 当前活跃的 watcher
  
  constructor() {
    this.id = Math.random();
    this.subs = []; // 订阅者数组(所有依赖这个属性的 watcher)
  }
  
  // 添加订阅者
  addSub(sub) {
    this.subs.push(sub);
  }
  
  // 移除订阅者
  removeSub(sub) {
    const index = this.subs.indexOf(sub);
    if (index > -1) {
      this.subs.splice(index, 1);
    }
  }
  
  // 依赖收集:如果当前有活跃的 watcher,则添加到 subs
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  
  // 派发更新:通知所有订阅者
  notify() {
    const subs = this.subs.slice(); // 浅拷贝,避免并发问题
    for (let i = 0; i < subs.length; i++) {
      subs[i].update();
    }
  }
}
第二步:定义 Watcher(观察者)
/**
 * Watcher - 观察者
 * 负责监听表达式或函数,当依赖变化时执行回调
 */
class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm;
    this.cb = cb;
    this.deps = []; // 记录所有依赖的 dep
    this.newDeps = []; // 新依赖列表(用于去重)
    
    // 解析表达式或函数
    this.getter = typeof expOrFn === 'function' 
      ? expOrFn 
      : this.parsePath(expOrFn);
    
    this.options = options;
    
    // 立即执行,触发依赖收集
    this.value = this.get();
  }
  
  // 解析路径表达式,如 'user.name'
  parsePath(path) {
    const segments = path.split('.');
    return function(obj) {
      let result = obj;
      for (const segment of segments) {
        result = result[segment];
      }
      return result;
    };
  }
  
  // 获取值,触发 getter 进行依赖收集
  get() {
    // 将当前 watcher 设置为全局活跃状态
    Dep.target = this;
    let value;
    
    try {
      // 执行 getter,会触发所有用到属性的 getter
      value = this.getter.call(this.vm, this.vm);
    } finally {
      // 完成后清除全局状态
      Dep.target = null;
    }
    
    return value;
  }
  
  // 更新方法
  update() {
    const oldValue = this.value;
    const newValue = this.get();
    
    // 执行回调
    this.cb.call(this.vm, newValue, oldValue);
  }
  
  // 添加依赖
  addDep(dep) {
    // 去重:同一个 dep 只添加一次
    if (!this.newDeps.includes(dep)) {
      this.newDeps.push(dep);
      dep.addSub(this);
    }
  }
  
  // 清理依赖(用于组件销毁时)
  cleanup() {
    for (const dep of this.deps) {
      dep.removeSub(this);
    }
    this.deps = this.newDeps;
    this.newDeps = [];
  }
}
第三步:defineReactive(核心)
/**
 * defineReactive - 定义响应式属性
 * 为对象的每个属性创建 dep,劫持 getter/setter
 */
function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  observe(val);
  
  // 创建依赖收集器
  const dep = new Dep();
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`📖 读取 ${key}: ${val}`);
      
      // 依赖收集:如果有当前 watcher,则添加到 dep
      if (Dep.target) {
        dep.depend();
      }
      
      return val;
    },
    set(newVal) {
      console.log(`✏️ 设置 ${key}: ${val} -> ${newVal}`);
      
      if (newVal === val) return;
      
      // 递归处理新值(可能是对象)
      observe(newVal);
      
      val = newVal;
      
      // 派发更新:通知所有订阅者
      dep.notify();
    }
  });
}
第四步:observe(观察入口)
/**
 * observe - 将对象转换为响应式
 */
function observe(value) {
  // 非对象或 null 直接返回
  if (!value || typeof value !== 'object') return;
  
  // 判断是否为数组
  if (Array.isArray(value)) {
    // 数组特殊处理:重写数组方法
    protoAugment(value, arrayMethods);
  } else {
    // 对象:遍历每个属性
    Object.keys(value).forEach(key => {
      defineReactive(value, key, value[key]);
    });
  }
}

// 辅助函数:原型链增强
function protoAugment(target, src) {
  target.__proto__ = src;
}
第五步:数组响应式处理
// 重写数组原型方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

// 需要重写的 7 个变更方法
const methodsToPatch = [
  'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
];

methodsToPatch.forEach(method => {
  const original = arrayProto[method];
  
  Object.defineProperty(arrayMethods, method, {
    value: function(...args) {
      console.log(`🔄 数组方法:${method}(${args.join(', ')})`);
      
      const result = original.apply(this, args);
      
      // 获取新增的元素
      let inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break;
        case 'splice':
          inserted = args.slice(2);
          break;
      }
      
      // 通知更新
      ob.dep.notify();
      
      // 如果是新增元素,也要转为响应式
      if (inserted) ob.observeArray(inserted);
      
      return result;
    },
    writable: true,
    configurable: true,
    enumerable: false
  });
});
第六步:完整示例
// 测试数据
const data = {
  name: 'Vue2',
  age: 3,
  hobbies: ['reading', 'coding']
};

// 转换为响应式
observe(data);

// 创建一个模拟的 vm 对象(模拟 Vue 实例)
const vm = {
  _data: data,
  // 代理属性,使得 vm.name === vm._data.name
  get name() { return this._data.name; },
  set name(val) { this._data.name = val; }
};

// 创建 watcher - 监听 vm.name
const watcher = new Watcher(vm, 'name', (newVal, oldVal) => {
  console.log(`✅ name 变化了:${oldVal} -> ${newVal}`);
});

console.log('\n--- 测试 1: 修改现有属性 ---');
data.name = 'Vue2 Updated'; // ✅ 触发更新

console.log('\n--- 测试 2: 读取属性 ---');
console.log('name:', data.name); // 📖 读取 name

console.log('\n--- 测试 3: 数组操作 ---');
data.hobbies.push('writing'); // ✅ 触发更新

console.log('\n--- 测试 4: 嵌套对象 ---');
const nestedData = { user: { name: 'Alice' } };
observe(nestedData);

// 为嵌套对象创建 vm
const nestedVm = {
  _data: nestedData,
  get user() { return this._data.user; }
};

const nestedWatcher = new Watcher(nestedVm, 'user.name', (newVal, oldVal) => {
  console.log(`✅ user.name 变化了:${oldVal} -> ${newVal}`);
});

nestedData.user.name = 'Bob'; // ✅ 触发更新

2.3 Vue 2 的缺陷

由于 Object.defineProperty() 的限制,Vue 2 存在以下问题:

  1. 无法检测对象属性的添加或删除

    data.newProp = 'test'; // ❌ 不会触发更新
    Vue.set(data, 'newProp', 'test'); // ✅ 需要使用 API
    
  2. 无法检测数组索引的变化

    data.hobbies[0] = 'swimming'; // ❌ 不会触发更新
    
  3. 初始化性能问题

    // 必须一次性遍历所有属性
    Object.keys(value).forEach(key => {
      defineReactive(value, key, value[key]);
    });
    

三、Vue 3 响应式原理:Proxy

3.1 核心优势

Vue 3 使用 ES6 的 Proxy,完美解决了 Vue 2 的所有缺陷:

  • ✅ 可拦截整个对象操作
  • ✅ 可检测属性的添加/删除
  • ✅ 可检测数组索引和 length 变化
  • ✅ 懒代理,性能更优

3.2 从零实现 Vue 3 响应式

第一步:WeakMap 存储依赖
/**
 * targetMap - 存储所有响应式对象的依赖关系
 * 结构:WeakMap<target, Map<key, Set<effect>>>
 */
const targetMap = new WeakMap();

// 当前激活的 effect
let activeEffect = null;
const effectStack = [];

// 特殊 key,用于追踪对象整体变化
const ITERATE_KEY = Symbol('iterate');
第二步:track(依赖收集)
/**
 * track - 依赖收集
 * 在 getter 时调用,记录当前 effect 依赖了这个属性
 */
function track(target, key) {
  // 如果没有活跃的 effect,不需要收集
  if (!activeEffect) return;
  
  // 从 targetMap 中获取 depsMap
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  // 从 depsMap 中获取 dep
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  
  // 添加当前 effect 到 dep
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    
    // effect 记录它依赖了哪些 dep(用于清理)
    activeEffect.deps.push(dep);
    
    console.log(`📎 收集依赖:${String(key)}`);
  }
}
第三步:trigger(派发更新)
/**
 * trigger - 派发更新
 * 在 setter 时调用,找到所有依赖这个属性的 effect 并执行
 */
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const effectsToRun = new Set();
  
  // 获取与 key 相关的依赖
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => {
      if (effect !== activeEffect) {
        effectsToRun.add(effect);
      }
    });
  }
  
  // 如果是数组长度变化
  if (Array.isArray(target) && key === 'length') {
    depsMap.forEach((dep, key) => {
      if (key >= activeEffect?.options?.oldLength || key === 'length') {
        dep.forEach(effect => {
          if (effect !== activeEffect) {
            effectsToRun.add(effect);
          }
        });
      }
    });
  }
  
  // 运行需要更新的 effect
  effectsToRun.forEach(effect => {
    console.log(`🔔 触发更新:${String(key)}`);
    
    if (effect.options.scheduler) {
      // 有调度器则使用调度器(用于批量更新)
      effect.options.scheduler(effect);
    } else {
      effect();
    }
  });
}
第四步:effect(副作用函数)
/**
 * effect - 创建响应式副作用函数
 * 类似 Vue 2 的 Watcher,但更轻量
 */
function effect(fn, options = {}) {
  const scheduler = options.scheduler || null;
  
  const effectFn = () => {
    try {
      // 清理旧的依赖(避免内存泄漏)
      cleanup(effectFn);
      
      // 设置当前 effect 为活跃状态
      activeEffect = effectFn;
      effectStack.push(effectFn);
      
      // 执行 fn,触发 getter 进行依赖收集
      return fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  };
  
  effectFn.deps = [];
  effectFn.options = options;
  
  // 立即执行
  if (!options.lazy) {
    effectFn();
  }
  
  return effectFn;
}

// 清理依赖
function cleanup(effectFn) {
  const { deps } = effectFn;
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effectFn);
    }
    deps.length = 0;
  }
}
第五步:reactive(核心实现)
/**
 * reactive - 创建响应式对象
 */
function reactive(target) {
  return createReactiveObject(target, false);
}

/**
 * readonly - 创建只读对象
 */
function readonly(target) {
  return createReactiveObject(target, true);
}

function createReactiveObject(target, isReadonly) {
  // 非对象直接返回
  if (!isObject(target)) return target;
  
  return new Proxy(target, {
    get(target, key, receiver) {
      console.log(`📖 Proxy get: ${String(key)}`);
      
      // 非只读模式下进行依赖收集
      if (!isReadonly) {
        track(target, key);
      }
      
      const res = Reflect.get(target, key, receiver);
      
      // 深度响应式:嵌套对象也转为 proxy
      if (isObject(res)) {
        return isReadonly ? readonly(res) : reactive(res);
      }
      
      return res;
    },
    
    set(target, key, value, receiver) {
      console.log(`✏️ Proxy set: ${String(key)} = ${value}`);
      
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      
      // 只在值变化时触发更新
      if (hasChanged(value, oldValue)) {
        trigger(target, key);
      }
      
      return result;
    },
    
    deleteProperty(target, key) {
      console.log(`🗑️ Proxy delete: ${String(key)}`);
      
      const hadKey = hasOwn(target, key);
      const result = Reflect.deleteProperty(target, key);
      
      // 删除存在的属性时触发更新
      if (hadKey && result) {
        trigger(target, key);
      }
      
      return result;
    },
    
    has(target, key) {
      const result = Reflect.has(target, key);
      if (!isReadonly) {
        track(target, key);
      }
      return result;
    },
    
    ownKeys(target) {
      const result = Reflect.ownKeys(target);
      if (!isReadonly) {
        track(target, ITERATE_KEY); // 追踪对象整体
      }
      return result;
    }
  });
}

// 辅助函数
function isObject(value) {
  return value !== null && typeof value === 'object';
}

function hasChanged(value, oldValue) {
  return value !== oldValue && (value === value || oldValue === oldValue);
}

function hasOwn(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}
第六步:完整示例
// 测试数据
const data = reactive({
  name: 'Vue3',
  age: 4,
  hobbies: ['reading', 'coding']
});

// 创建 effect
effect(() => {
  console.log(`✅ effect 执行:name is ${data.name}`);
});

console.log('\n--- 测试 1: 修改现有属性 ---');
data.name = 'Vue3 Updated'; // ✅ 触发更新

console.log('\n--- 测试 2: 添加新属性 ---');
data.newProp = 'test'; // ✅ 自动响应式

console.log('\n--- 测试 3: 删除属性 ---');
delete data.age; // ✅ 触发更新

console.log('\n--- 测试 4: 数组索引修改 ---');
data.hobbies[0] = 'swimming'; // ✅ 触发更新

console.log('\n--- 测试 5: 数组 push ---');
data.hobbies.push('writing'); // ✅ 触发更新

console.log('\n--- 测试 6: 嵌套对象 ---');
const nestedData = reactive({ user: { name: 'Alice' } });

effect(() => {
  console.log(`✅ nested effect: user.name is ${nestedData.user.name}`);
});

nestedData.user.name = 'Bob'; // ✅ 触发更新

四、Vue 2 vs Vue 3:全方位对比

4.1 功能对比

特性Vue 2Vue 3
对象属性增删❌ 不支持✅ 支持
数组索引修改❌ 不支持✅ 支持
Map/Set 支持❌ 不支持✅ 支持
TypeScript 支持⭐⭐⭐⭐⭐⭐⭐

4.2 性能对比

指标Vue 2Vue 3
初始化速度慢(遍历所有属性)快(懒代理)
内存占用高(每个属性都有 dep)低(WeakMap)
更新精度粗粒度细粒度

4.3 代码量对比

// Vue 2: 需要复杂的初始化
function initReactive(obj) {
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

// Vue 3: 一行搞定
const state = reactive({});

五、实战技巧

5.1 调试技巧

// 添加日志,查看依赖收集过程
function track(target, key) {
  console.group('📎 Track');
  console.log('Target:', target);
  console.log('Key:', key);
  console.log('Active Effect:', activeEffect);
  console.groupEnd();
}

// 添加日志,查看触发更新过程
function trigger(target, key) {
  console.group('🔔 Trigger');
  console.log('Target:', target);
  console.log('Key:', key);
  console.log('Effects to run:', effectsToRun.size);
  console.groupEnd();
}

5.2 常见面试题

Q1: 为什么 Vue 3 使用 WeakMap?

A: WeakMap 的键是弱引用,当对象没有其他地方引用时,可以被垃圾回收,避免内存泄漏。

Q2: Vue 2 如何实现数组响应式?

A: 重写数组的 7 个变更方法(push、pop、shift、unshift、splice、sort、reverse),在这些方法内部触发通知。

Q3: Proxy 相比 Object.defineProperty 有什么优势?

A:

  1. 可以直接监听对象而非属性
  2. 可以监听属性的添加和删除
  3. 可以监听数组索引和 length 变化
  4. 支持 Map、Set 等数据结构

六、总结

通过本文的学习,你应该掌握了:

  1. ✅ Vue 2 使用 Object.defineProperty 劫持 getter/setter
  2. ✅ Vue 3 使用 Proxy 拦截整个对象
  3. ✅ 依赖收集和派发更新的完整流程
  4. ✅ 两种实现方式的优缺点对比

响应式系统是 Vue 的灵魂所在。理解其原理,不仅能帮助你更好地使用 Vue,还能提升你的架构设计能力。


附录:完整可运行代码

所有代码示例都可以在浏览器控制台直接运行。建议你将代码复制到 CodePen 或本地文件中,亲自动手实践,加深理解。

思考题

  1. 如果要支持 Map 和 Set 的响应式,应该如何实现?
  2. Vue 3 的 computed 是如何基于 effect 实现的?
  3. 如何实现一个支持批量的更新调度器?

欢迎在评论区分享你的答案!


参考资料


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!