Vue2与Vue3响应式原理对比
Vue2和Vue3能够实现响应式系统的核心API分别是:Object.defineProperty() 和 new Proxy() 。简单来说,它们的作用都是:拦截和监听对象的变化,当数据被读取或修改时,能够"感知"到并进行后续操作(例如:更新视图)。
一、Vue2 的实现:Object.defineProperty()
Object.defineProperty()
是 ES5 中的一个方法,它可以直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性,并返回这个对象。
1. 它是如何工作的?
Vue2 使用 Object.defineProperty()
来递归地遍历数据对象的所有属性,将它们转换为 getter 和 setter。
- getter : 当属性被读取时触发。在这里收集依赖(例如:哪个组件用到了这个数据)。
- setter : 当属性被修改时触发。在这里通知所有依赖进行更新(例如:通知组件重新渲染)。
2. 代码示例
js
// 一个简单的数据对象
const data = {
count: 0
};
// 模拟 Vue2 的响应式转换
let internalValue = data.count; // 内部存储实际值
Object.defineProperty(data, 'count', {
get() {
console.log(`属性被读取了,当前值是: ${internalValue}`);
return internalValue; // 读取时返回内部值
},
set(newValue) {
console.log(`属性被修改了,新值是: ${newValue}`);
internalValue = newValue;
// 在这里,Vue 会通知所有依赖于此属性的地方进行更新
// updateView(); // 假设这是一个更新视图的函数
}
});
// 测试
console.log(data.count); // 输出: "属性被读取了...",然后输出 0
data.count = 5; // 输出: "属性被修改了..."
3. 缺点与局限性
- 对数组的限制 :
Object.defineProperty()
无法拦截数组的索引操作 (如arr[0] = newValue
)和修改长度的操作 (如arr.length = 0
)。Vue2 通过重写数组的 7 个方法(push
,pop
,shift
,unshift
,splice
,sort
,reverse
)来 hack 实现数组的响应式。这就是为什么在Vue2中,简单地修改修改数组会发现视图并没有改变。 - 初始化递归遍历: Vue2 在初始化时需要递归地遍历对象的所有属性并将其转换为 getter/setter,如果对象嵌套很深,性能开销较大。
- 无法监听对象属性的添加或删除 : 由于
Object.defineProperty()
是针对已知属性 操作的,所以无法拦截给对象新增属性 (obj.newProperty = value
)或删除属性 (delete obj.property
)的操作。这就是为什么 Vue2 提供了Vue.set
和Vue.delete
这些特殊 API 的原因。
二、Vue3 的实现:new Proxy()
Proxy
是 ES6 引入的一个全新功能。它可以创建一个对象的代理 ,从而允许我们拦截并重新定义对这个对象的基本操作。
1. 它是如何工作的?
Vue3 使用 Proxy
来"包装"整个目标对象。我们可以为这个代理对象设置一系列的"陷阱"(traps),这些陷阱可以拦截对目标对象的多种操作,而不仅仅是 get
和 set
。
- get trap : 拦截任何属性的读取操作,包括那些不存在的属性。
- set trap : 拦截任何属性的设置操作。
- deleteProperty trap : 拦截属性的删除操作 (
delete obj.property
)。 - ...以及其他操作(如
has
,ownKeys
等)。
2. 代码示例
js
// 原始数据对象
const data = {
count: 0
};
// 使用 Proxy 创建代理
const reactiveData = new Proxy(data, {
get(target, property) {
console.log(`读取了属性 ${property}, 值是: ${target[property]}`);
return target[property];
},
set(target, property, value) {
console.log(`修改了属性 ${property}, 新值为: ${value}`);
target[property] = value;
// 通知更新
// triggerUpdate();
return true; // 表示设置成功
},
deleteProperty(target, property) {
console.log(`删除了属性: ${property}`);
delete target[property];
// 通知更新
// triggerUpdate();
return true; // 表示删除成功
}
});
// 测试
console.log(reactiveData.count); // 输出: "读取了属性 count...",然后输出 0
reactiveData.count = 5; // 输出: "修改了属性 count..."
reactiveData.newProp = "Hello"; // 输出: "修改了属性 newProp..." (Vue2 无法监听到!)
delete reactiveData.newProp; // 输出: "删除了属性: newProp" (Vue2 无法监听到!)
3. 优势
-
全能拦截 : 可以拦截对象上的几乎所有操作 (包括数组索引、
length
、属性的新增和删除),不再需要 Vue2 的那些 hack 和特殊 API。 -
性能更好:
- 惰性递归: Proxy 只在 get 拦截到嵌套对象时才会递归代理下一层,而不是像 Vue2 那样一次性递归所有层,初始化性能更好。
- 更好的数组支持: 天然支持数组的各种操作。
-
更友好的 API : 因为可以拦截
delete
和新增操作,所以开发者不再需要记忆Vue.set
和Vue.delete
,直接操作对象即可。