Vue 的数据响应式原理
我之前一直很好奇在Vue中是如何实现数据的响应式的,从我自己的角度这个问题其实很好理解,实现数据的响应式,无非就是监听数据的变化嘛,这边儿数据变了,那边儿更新对应的引用数据的位置就可以了。
但是仔细思考一下,又感觉没那么容易。数据监听应该如何实现?引用位置如何确定?
在React
中我发现这个问题更加离谱,他在数据更新以后,把他的所有子组件全刷新了一遍,重新执行了一遍所有没带缓存的子组件函数的render
,那在Vue总这个问题是否也存在呢?
Object.defineProperty
在JavaScript中,为我们提供了类似Object.defineProperty
的方式,可以轻松的监听的数据变化,回顾了一下它的用法。
Object.defineProperty(obj, prop, descriptor)
Code language: JavaScript (javascript)
- obj: 要定义属性的对象。
- prop: 属性的名字。
- descriptor: 属性的描述符对象,包含了一些可以控制属性行为的配置选项。
我们先来试试水
- 定义一个简单属性
const obj = {}<br>Object.defineProperty(obj, 'name', {<br> value: 'Nic'<br>})<br><br>console.log("Name =", obj.name);<br>// >>> Name = Nic<br><br>console.log(Object.keys(obj)) // 直接通过Object.defineProperty 定义不会进入枚举里被奥<br>// >>> {}<br>
也就是说,这个方法可以直接定义 Object
的属性,并且可以绕开一些默认的描述,比如枚举,那我们通过描述符是不是还可以干一些其他事情,先来看看它的描述符号有些什么
常见的描述符属性
- value: 属性的值(可以是任何类型)。
- writable: 如果为
true
,属性值可以被修改(默认为false
)。 - enumerable: 如果为
true
,该属性会出现在for...in
循环以及Object.keys()
等方法的结果中(默认为false
)。 - configurable: 如果为
true
,可以删除或修改属性的描述符(默认为false
)。 - get: 用于获取属性值的 getter 函数。
- set: 用于设置属性值的 setter 函数。
那我们试试,实现一个不可被修改的属性值
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'John',
writable: false, // 不允许修改
});
console.log(obj.name); // "John"
obj.name = 'Doe'; // 修改失败,静默失败
console.log(obj.name); // "John"
Code language: JavaScript (javascript)
确实无法被修改了。
那我们定义一个属性的时候,定义它的get
和set
函数,是不是就可以监听到它的变化了?
定义一个getter
和一个setter
const obj = {
firstName: 'John',
lastName: 'Doe'
};
// 使用 getter 和 setter 定义计算属性
Object.defineProperty(obj, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
},
enumerable: true,
configurable: true
});
console.log(obj.fullName); // "John Doe"
obj.fullName = 'Jane Smith'; // 使用 setter 设置新值
console.log(obj.firstName); // "Jane"
console.log(obj.lastName); // "Smith"
Code language: JavaScript (javascript)
嗯,很好,我们修改了呃一个属性的get和set 以后,确实能够监听到数据的改变,那如果我再配合上发布订阅模式,是不是就可以实现基础的数据监听了。
虽然,这个方案确实可以监听的数据变化,但是Vue在升级Vue3以后就抛弃了这种实现方案,改用全新的特性Proxy
说明这种方案应该有一些缺点
1.Proxy 能监听整个对象,而**Object.defineProperty
**只能监听单个属性
Object.defineProperty
只能对对象的每个属性单独进行劫持,不能直接监听对象本身。如果一个对象有很多属性,Vue 2 需要遍历每个属性并调用Object.defineProperty
。Proxy
直接作用于整个对象,不需要遍历每个属性,可以拦截对整个对象的操作,包括属性添加和删除。
Vue 2 示例(**Object.defineProperty
**监听属性):
javascript
复制编辑
const data = {};
Object.defineProperty(data, 'msg', {
get() {
console.log('获取 msg');
return 'Hello';
},
set(newVal) {
console.log('设置 msg:', newVal);
}
});
console.log(data.msg); // 获取 msg
data.msg = 'Hi'; // 设置 msg: Hi
Code language: JavaScript (javascript)
这个方法只能劫持msg
,如果data
还有别的属性,就需要再手动定义。
Vue 3 示例(**Proxy
**监听整个对象):
const data = new Proxy({}, {
get(target, key) {
console.log(`获取 ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`设置 ${key} 为 ${value}`);
target[key] = value;
return true;
}
});
console.log(data.msg); // 获取 msg
data.msg = 'Hi'; // 设置 msg 为 Hi
Code language: JavaScript (javascript)
这个Proxy
可以监听所有属性的访问和修改,不需要为每个属性单独定义 getter 和 setter。
2.Proxy 能检测属性的添加和删除
- Vue 2 使用
Object.defineProperty
监听对象时,无法检测到新增或删除属性,必须使用Vue.set()
才能让 Vue 监听新加的属性。 - Vue 3 使用
Proxy
可以直接监听对象的get
、set
、deleteProperty
操作,不需要额外的方法。
Vue 2(无法监听新增属性)
const obj = {};
Object.defineProperty(obj, 'msg', {
value: 'Hello',
writable: true,
configurable: true
});
// Vue 2 无法监听新增的属性
obj.newProp = 'Hi';
console.log(obj.newProp); // Hi,但 Vue 2 不会响应这个变化
Code language: JavaScript (javascript)
必须用Vue.set(obj, 'newProp', 'Hi')
才能让 Vue 监听到新增的属性。
Vue 3(**Proxy
**可以监听新增和删除)
const obj = new Proxy({}, {
get(target, key) {
return target[key];
},
set(target, key, value) {
console.log(`设置 ${key} 为 ${value}`);
target[key] = value;
return true;
},
deleteProperty(target, key) {
console.log(`删除属性 ${key}`);
delete target[key];
return true;
}
});
obj.msg = 'Hello'; // 设置 msg 为 Hello
delete obj.msg; // 删除属性 msg
Code language: JavaScript (javascript)
在 Vue 3 中,你可以直接删除属性,而 Vue 2 无法做到这一点。
3.Proxy 支持对数组的监听
- Vue 2 无法监听数组索引的变化,比如
arr[1] = 'newValue'
,需要使用Vue.set(arr, 1, 'newValue')
才能让 Vue 监听到变化。 - Vue 3 通过
Proxy
直接监听数组的所有操作,如push
、pop
、splice
等。
Vue 2(无法监听数组索引变化)
const arr = [];
Object.defineProperty(arr, '0', {
get() {
console.log('获取索引 0');
return 'Hello';
},
set(newVal) {
console.log('设置索引 0:', newVal);
}
});
arr[0] = 'Hi'; // Vue 2 无法监听
console.log(arr[0]); // 仍然是 'Hi',但 Vue 2 响应式系统不会检测到这个变化
Code language: JavaScript (javascript)
Vue 3(**Proxy
**监听数组)
const arr = new Proxy([], {
set(target, key, value) {
console.log(`数组索引 ${key} 变为 ${value}`);
target[key] = value;
return true;
}
});
arr[0] = 'Hi'; // 数组索引 0 变为 Hi
console.log(arr[0]); // Hi
Code language: JavaScript (javascript)
Vue 3 可以监听数组的索引修改和方法调用(push
、pop
等),不需要额外的Vue.set()
。
4.*Proxy 允许监听对象的原型 (***__proto__
**修改)
Vue 2 由于使用Object.defineProperty
,无法检测对象的__proto__
变化。例如:
const obj = { a: 1 };
Object.setPrototypeOf(obj, { b: 2 });
console.log(obj.b); // 2,但 Vue 2 无法检测到 b 这个新属性
Code language: JavaScript (javascript)
Vue 3 由于Proxy
拦截了get
操作,可以监听__proto__
的变化,从而提高响应式能力。
5.Proxy 性能更好
在 Vue 2 中,Object.defineProperty
需要对每个属性进行遍历和深度绑定,如果对象有很多属性,初始化开销很大。而Proxy
直接作用于整个对象,不需要遍历所有属性,因此性能更好。
6.Proxy 未来可扩展性更强
Object.defineProperty
只能拦截get
和set
,而Proxy
提供了更多的拦截方法,如:
getPrototypeOf
/setPrototypeOf
has
(用于in
操作符)ownKeys
(用于Object.keys()
)apply
(拦截函数调用)construct
(拦截new
操作)
这些特性让 Vue 3 的响应式系统更强大,更容易扩展。
总结:为什么 Vue 3 使用Proxy
特性 | `Object.defineProperty`(Vue 2) | `Proxy`(Vue 3) |
---|---|---|
监听整个对象 | ❌ 只能逐个属性监听 | ✅ 直接监听整个对象 |
监听新增/删除属性 | ❌ 需要`Vue.set()` | ✅ 直接支持 |
监听数组索引变化 | ❌ 需要`Vue.set()` | ✅ 直接支持 |
监听`__proto__`变化 | ❌ 无法监听 | ✅ 可以监听 |
性能 | 🚀 需要遍历所有属性,性能低 | ⚡ 直接作用于整个对象,性能更好 |
未来可扩展性 | 🚫 仅支持`get`、`set` | ✅ 拦截更多操作,如`has`、`ownKeys` |
由于这些优势,Vue 3 选择了Proxy
来实现响应式,而不再使用Object.defineProperty
。但Proxy
需要 ES6+,在 IE11 及更早版本中不受支持,因此 Vue 3不再兼容 IE11。
依赖收集
数据更新以后,就需要通知到视图层去刷新页面的数据。一个数据变化,应该直接作用在使用数据的位置,也就是说谁动了谁更新,
暂无评论
要发表评论,您必须先 登录