参考文章:
Object.defineProperty是数据劫持,Proxy是代理。
# 实现
# vue2的defineProperty实现
const data = { name: 'xxx' };
function defineReactive(data, key, val) {
observe(val); // 监听子属性
Object.defineProperty(data, key, {
configurable: true, // 属性可删除
enumerable: true, // 可枚举
get() {
return val;
},
set(newVal) {
console.log('set', val, '-->', newVal);
val = newVal;
},
});
}
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
// 取出所有属性遍历
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
observe(data);
data.name = 'OOO'; // set xxx --> OOO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# vue3的Proxy实现
const data = { name: 'xxx' };
const handler = {
get(target, key) {
console.log(`${key} 被读取`);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log(`${key} 被设置为 ${value}`);
Reflect.set(target, key, value);
},
};
function observe(data) {
if (!data || typeof data !== 'object') return false;
// 如果对象的属性还是引用值,那么还是需要递归的去监听
const proxyData = new Proxy(data, handler);
return proxyData;
}
const proxyData = observe(data);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 为什么Vue3用Proxy性能更好
Proxy可以直接监听数组的变化;Proxy可以监听对象而非属性。它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy直接可以劫持整个对象,并返回一个新对象。Proxy对代理对象的监听更加丰富。有且不限于以下场景:Proxy可以监听对象新属性的定义(construct)和函数操作的调用(apply)、ownKeys、deleteProperty、has,而Object.defineProperty无法实现。
# Object.defineProperty和Proxy的缺点是什么
# Object.defineProperty的缺点
- 无法检测到对象属性的新增或删除,如果有新增属性需要实现双向绑定则需要手动调用
Vue.set()/vm.$set();只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。 - 无法监听数组变化,文档所说的能够监听七种数组操作(
push、pop、unshift、shift、reverse、sort、splice),这都是作者重写了这些方法。
// src/core/observer/array.js
// 获取数组的原型Array.prototype,上面有我们常用的数组方法
const arrayProto = Array.prototype;
// 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 列出需要重写的数组方法名
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
];
// 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
methodsToPatch.forEach((method) => {
// 保存一份当前的方法名对应的数组原始方法
const original = arrayProto[method];
// 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
def(arrayMethods, method, function mutator(...args) {
// 调用数组原始方法,并传入参数args,并将执行结果赋给result
const result = original.apply(this, args);
// 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 如果有新增元素,则给新增的元素添加监听
if (inserted) ob.observeArray(inserted);
// 将当前数组的变更通知给其订阅者
ob.dep.notify();
// 最后返回执行结果result
return result;
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
- 对于响应式数组,当浏览器支持
__proto__属性时,使用push等方法时先从其原型arrayMethods上寻找push方法,也就是重写后的方法,处理之后数组的变化会通知到其订阅者,更新页面,当在arrayMethods上查询不到时会向上在Array.prototype上查询;当浏览器不支持__proto__属性时,使用push等方法时会先从数组自身上查询,如果查询不到会向上再Array.prototype上查询。 - 对于非响应式数组,当使用
push等方法时会直接从Array.prototype上查询。
# Proxy的缺点
Proxy的劣势就是兼容性问题,而且无法用polyfill实现。
# Object.defineProperty是一种怎样的机制
- 无需显示调用: 例如Vue运用数据劫持+发布订阅,直接可以通知变化并驱动视图。
- 可精确得知变化数据:劫持了属性的setter,当属性值改变,我们可以精确获知变化的内容newVal,因此在这部分不需要额外的diff操作,否则我们只知道数据发生了变化而不知道具体哪些数据变化了,这个时候需要大量diff来找出变化值,这是额外性能损耗。
# 题外话
其他能够监听对象属性变化的方法
Object.observe()方法用于异步地监视一个对象的修改。当对象属性被修改时,方法的回调函数会提供一个有序的修改流。然而,这个接口已经被废弃并从各浏览器中移除。你可以使用更通用的Proxy对象替代。Object.prototype.watch()方法会监视属性是否被赋值并在赋值时运行相关函数。警告: 通常来讲,你应该尽量避免使用
watch()和unwatch()这两个方法。因为只有 Gecko 实现了这两个方法,并且它们主要是为了在调试方便。另外,使用 watchpoint 对性能有严重的负面影响,在全局对象(如 window)上使用时尤其如此。你可以使用setters and getters或者Proxy代替。参见 Compatibility (opens new window) 了解详情。