Vue2 的响应式实现(Object.defineProperty)
Vue2 的响应式核心是 Object.defineProperty 这个 ES5 原生 API,它的核心思路是遍历对象的每个属性,为其添加 getter/setter 拦截。
1. 核心实现逻辑
// 模拟 Vue2 响应式核心逻辑
function defineReactive(obj, key, value) {
// 递归处理嵌套对象(比如 obj = { a: { b: 1 } })
observe(value);
// 拦截属性的读取和修改
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
// 读取属性时触发(收集依赖)
get() {
console.log(`读取 ${key}:${value}`);
// 收集依赖(比如对应的 watcher/effect)
dep.depend();
return value;
},
// 修改属性时触发(触发更新)
set(newVal) {
if (newVal === value) return;
console.log(`修改 ${key}:${newVal}`);
value = newVal;
// 递归处理新值(如果新值是对象)
observe(newVal);
// 通知依赖更新(比如重新渲染视图)
dep.notify();
}
});
}
// 遍历对象,为所有属性添加响应式
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
// 遍历对象的每个 key
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 测试
const data = { price: 100, number: 2 };
observe(data);
// 读取属性 → 触发 get
console.log(data.price); // 输出:读取 price:100 100
// 修改属性 → 触发 set
data.number = 3; // 输出:修改 number:3
2. Vue2 响应式的关键特点
- 拦截粒度:针对对象的单个属性拦截,需要遍历对象的所有属性(包括嵌套对象递归遍历)。
- 依赖收集:读取属性(get)时,收集当前的依赖(Watcher);修改属性(set)时,通知所有收集的 Watcher 执行更新。
- 局限性(也是 Vue2 响应式的痛点):
- 无法监听新增属性(比如 data.newKey = 123,因为初始化时没为这个属性加 getter/setter),需要用 this.$set。
- 无法监听数组的下标 / 长度修改(比如 arr[0] = 1、arr.length = 0),Vue2 只能重写数组的 7 个方法(push/pop/shift/unshift/splice/sort/reverse)来监听。
- 必须提前遍历所有属性,对性能有一定影响(尤其是深层嵌套对象)。
二、Vue3 的响应式实现(Proxy)
Vue3 放弃了 Object.defineProperty,改用 ES6 新增的 Proxy API,核心思路是拦截整个对象的操作,而非单个属性。
1. 核心实现逻辑
// 模拟 Vue3 响应式核心逻辑
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
// 创建 Proxy 代理对象
return new Proxy(obj, {
// 读取属性时触发(包括 obj.key、obj['key']、数组下标)
get(target, key, receiver) {
console.log(`读取 ${key}`);
// 收集依赖
track(target, key);
// 处理嵌套对象(比如 obj.a.b,递归返回代理对象)
const res = Reflect.get(target, key, receiver);
return reactive(res);
},
// 修改属性/新增属性时触发
set(target, key, value, receiver) {
console.log(`修改/新增 ${key}:${value}`);
// 先执行原始赋值操作
const oldVal = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
// 只有值变化时才触发更新
if (oldVal !== value) {
// 触发依赖更新
trigger(target, key);
}
return result;
},
// 删除属性时触发
deleteProperty(target, key) {
console.log(`删除 ${key}`);
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
}
});
}
// 模拟依赖收集
function track(target, key) {
console.log(`收集 ${key} 的依赖`);
}
// 模拟触发更新
function trigger(target, key) {
console.log(`触发 ${key} 的依赖更新`);
}
// 测试
const data = reactive({ price: 100, number: 2, arr: [1,2,3] });
// 读取属性 → 触发 get
console.log(data.price); // 输出:读取 price 收集 price 的依赖 100
// 新增属性 → 触发 set(Vue2 做不到)
data.newKey = 'abc'; // 输出:修改/新增 newKey:abc 触发 newKey 的依赖更新
// 修改数组下标 → 触发 set(Vue2 做不到)
data.arr[0] = 10; // 输出:读取 arr 收集 arr 的依赖 读取 0 收集 0 的依赖 修改/新增 0:10 触发 0 的依赖更新
// 删除属性 → 触发 deleteProperty
delete data.number; // 输出:删除 number 触发 number 的依赖更新
2. Vue3 响应式的关键特点
- 拦截粒度:针对整个对象拦截,无需提前遍历所有属性(访问属性时才会递归处理嵌套对象)。
- 核心优势:
- 支持监听新增属性(比如 obj.newKey = 123),无需额外 API。
- 支持监听数组的下标 / 长度修改(比如 arr[0] = 1、arr.length = 0)。
- 支持拦截 delete 操作、in 操作等更多对象行为。
- 性能更优:懒递归(访问嵌套属性时才递归代理,而非初始化时全量遍历)。
注意:Proxy 是 ES6 特性,无法兼容 IE 浏览器,这也是 Vue3 放弃 IE 支持的原因之一。
receiver有什么用?
不用 receiver 会发生什么?
我们先把代码改成 return target[key](去掉 receiver),运行后看结果:
const p1 = {
lastName: 'shuang',
firstName: 'xunian',
get fullName() {
console.log('fullName 中的 this:', this); // 新增打印 this
return `${this.lastName} ${this.firstName}`
}
}
const nameProxy = new Proxy(p1, {
get(target, key, receiver) {
console.log('get', key);
return target[key]; // 不用 Reflect.get + receiver
},
set(target, key, value) {
console.log('set', key, value);
target[key] = value;
return true;
}
})
console.log(nameProxy.fullName);
输出结果:
get fullName
fullName 中的 this: { lastName: 'shuang', firstName: 'xunian', fullName: [Getter] }
shuang xunian
这里能发现两个问题:
- fullName 的 getter 中,this 指向的是原始对象 p1,而不是代理对象 nameProxy;
- 读取 this.lastName 和 this.firstName 时,没有触发 Proxy 的 get 拦截器(控制台只打印了 get fullName,没打印 get lastName/get firstName)。
用 Reflect.get + receiver
恢复 return Reflect.get(target, key, receiver),再运行:
const p1 = {
lastName: 'shuang',
firstName: 'xunian',
get fullName() {
console.log('fullName 中的 this:', this);
return `${this.lastName} ${this.firstName}`
}
}
const nameProxy = new Proxy(p1, {
get(target, key, receiver) {
console.log('get', key);
return Reflect.get(target, key, receiver); // 带 receiver
},
set(target, key, value) {
console.log('set', key, value);
target[key] = value;
return true;
}
})
console.log(nameProxy.fullName);
输出结果:
get fullName
fullName 中的 this: Proxy { lastName: 'shuang', firstName: 'xunian', fullName: [Getter] }
get lastName
get firstName
shuang xunian
核心变化:
- fullName 的 getter 中,this 指向的是代理对象 nameProxy,而非原始对象 p1;
- 读取 this.lastName/this.firstName 时,触发了 Proxy 的 get 拦截器(打印了 get lastName/get firstName)。
receiver 的核心意义
receiver 本质是指定 getter 执行时的 this 指向,它的取值是:
- 当你访问 nameProxy.fullName 时,receiver 就是 nameProxy(代理对象本身);
- Reflect.get(target, key, receiver) 等价于:调用 target[key] 的 getter,并把 getter 中的 this 绑定为 receiver。
为什么 Vue3 必须用 receiver?
Vue3 的响应式核心是追踪所有依赖:
- 不仅要追踪 fullName 被访问,还要追踪 fullName 依赖的 lastName/firstName 被访问;
- 如果 getter 中的 this 指向原始对象 p1,那么访问 this.lastName 就是直接访问原始对象,不会经过 Proxy 的 get 拦截器,Vue 就无法收集到 lastName 这个依赖;
- 只有让 this 指向代理对象,访问 this.lastName 才会触发 Proxy 拦截,Vue 才能完整收集所有依赖,保证后续修改 lastName 时,能触发 fullName 的更新。
再举一个更直观的例子:修改属性后验证
// 接上面的代码,修改 lastName
nameProxy.lastName = 'zhang';
console.log(nameProxy.fullName);
用 receiver 的输出:
set lastName zhang
get fullName
fullName 中的 this: Proxy { lastName: 'zhang', firstName: 'xunian', fullName: [Getter] }
get lastName
get firstName
zhang xunian
不用 receiver 的输出:
set lastName zhang
get fullName
fullName 中的 this: { lastName: 'shuang', firstName: 'xunian', fullName: [Getter] }
shuang xunian // 这里还是旧值!!
问题根源:
不用 receiver 时,getter 中的 this 是原始对象 p1,而你修改的是代理对象 nameProxy 的 lastName(虽然最终同步到了 p1,但 getter 执行时 this 指向错误,导致读取的是原始对象的旧值 —— 这个例子可能因为对象是引用类型暂时不明显,但如果是嵌套 Proxy 场景,会直接导致响应式失效)。
总结
- receiver 的核心作用是保证 getter 执行时 this 指向代理对象,而非原始对象;
- 没有 receiver 时,getter 中访问的属性会绕过 Proxy 拦截,Vue 无法收集完整依赖,导致响应式失效;
- Vue3 中用 Reflect.get(target, key, receiver) 而非 target[key],是为了完整拦截所有属性访问,确保响应式依赖追踪的准确性,这是实现复杂响应式(比如计算属性)的基础。
- 简单来说,receiver 是 Proxy 和 Reflect 配合的 “桥梁”,保证了拦截逻辑的完整性,也是 Vue3 响应式系统能正确处理计算属性、嵌套对象的关键。