深入理解 Vue 响应式系统:从 Vue 2 到 Vue 3 的演进之路
摘要:响应式系统是 Vue.js 的核心特性之一。本文将带你从零开始实现一个完整的响应式系统,深入理解 Vue 2 和 Vue 3 的实现原理差异,并附上完整可运行的代码示例。
一、什么是响应式?
简单来说,响应式就是数据变化时,视图自动更新。
javascript
// 理想中的响应式
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(依赖收集器)
javascript
/**
* 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(观察者)
javascript
/**
* 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(核心)
javascript
/**
* 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(观察入口)
javascript
/**
* 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;
}
第五步:数组响应式处理
javascript
// 重写数组原型方法
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
});
});
第六步:完整示例
js
// 测试数据
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 存在以下问题:
-
无法检测对象属性的添加或删除
javascriptdata.newProp = 'test'; // ❌ 不会触发更新 Vue.set(data, 'newProp', 'test'); // ✅ 需要使用 API -
无法检测数组索引的变化
javascriptdata.hobbies[0] = 'swimming'; // ❌ 不会触发更新 -
初始化性能问题
javascript// 必须一次性遍历所有属性 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 存储依赖
javascript
/**
* targetMap - 存储所有响应式对象的依赖关系
* 结构:WeakMap<target, Map<key, Set<effect>>>
*/
const targetMap = new WeakMap();
// 当前激活的 effect
let activeEffect = null;
const effectStack = [];
// 特殊 key,用于追踪对象整体变化
const ITERATE_KEY = Symbol('iterate');
第二步:track(依赖收集)
javascript
/**
* 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(派发更新)
javascript
/**
* 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(副作用函数)
javascript
/**
* 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(核心实现)
javascript
/**
* 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);
}
第六步:完整示例
javascript
// 测试数据
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 2 | Vue 3 |
|---|---|---|
| 对象属性增删 | ❌ 不支持 | ✅ 支持 |
| 数组索引修改 | ❌ 不支持 | ✅ 支持 |
| Map/Set 支持 | ❌ 不支持 | ✅ 支持 |
| TypeScript 支持 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
4.2 性能对比
| 指标 | Vue 2 | Vue 3 |
|---|---|---|
| 初始化速度 | 慢(遍历所有属性) | 快(懒代理) |
| 内存占用 | 高(每个属性都有 dep) | 低(WeakMap) |
| 更新精度 | 粗粒度 | 细粒度 |
4.3 代码量对比
ini
// Vue 2: 需要复杂的初始化
function initReactive(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// Vue 3: 一行搞定
const state = reactive({});
五、实战技巧
5.1 调试技巧
javascript
// 添加日志,查看依赖收集过程
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:
- 可以直接监听对象而非属性
- 可以监听属性的添加和删除
- 可以监听数组索引和 length 变化
- 支持 Map、Set 等数据结构
六、总结
通过本文的学习,你应该掌握了:
- ✅ Vue 2 使用
Object.defineProperty劫持 getter/setter - ✅ Vue 3 使用
Proxy拦截整个对象 - ✅ 依赖收集和派发更新的完整流程
- ✅ 两种实现方式的优缺点对比
响应式系统是 Vue 的灵魂所在。理解其原理,不仅能帮助你更好地使用 Vue,还能提升你的架构设计能力。
附录:完整可运行代码
所有代码示例都可以在浏览器控制台直接运行。建议你将代码复制到 CodePen 或本地文件中,亲自动手实践,加深理解。
思考题:
- 如果要支持 Map 和 Set 的响应式,应该如何实现?
- Vue 3 的 computed 是如何基于 effect 实现的?
- 如何实现一个支持批量的更新调度器?
欢迎在评论区分享你的答案!
参考资料:
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!