数据劫持
使用过 Vue 框架的应该就不陌生了,因为 Vue 中的响应式原理就是 数据劫持 和 订阅发布模式。而这里主要讲的是 数据劫持。
数据劫持:在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。最典型的使用场景就是响应式。
- 在 Vue 2.x 利用
Object.defineProperty()
。(具体想知道 vue 中怎么实现的可以百度一下,网上都一大把) - 在 Vue 3.x 版本之后改用 Proxy 进行实现。
基础
首先说一下两个语法:
Object.defineProperty(obj, prop, descriptor)
该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
obj
要定义属性的对象prop
要定义或修改的属性的名称或Symbol
descriptor
要定义或修改的属性描述符返回值:被传递给函数的对象
详情 → 传送门
小DEMO
javascript
const data = {}
let a = 2;
Object.defineProperty(data, 'a', {
enumerable: true,
configurable: false,
get: () => {
console.log('get:', a)
return a
},
set: (newValue) => {
console.log('set:', newValue)
a = newValue
}
})
data.a = 10
console.log(data.a)
// 输出:
// set: 10
// get: { a: 10 }
// 10
new Proxy(target, handler)
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
target
要使用Proxy
包装的目标对象(可以使任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p
的行为。
详情 → 传送门
小DEMO
javascript
const handle = {
get(obj, prop, value) {
console.log('get:', value)
return value
},
set(obj, prop, value) {
obj[prop] = value
console.log('set:', value)
return true
}
}
const data = {
a: 1
}
const proxy = new Proxy(data, handle);
proxy.a = 10
console.log(proxy.a)
// 输出:
// set: 10
// get: { a: 10 }
// 10
Object.defineProperty
对比 Proxy
Object.defineProperty
的问题有三个:
- 不能监听数组的变化(如:push, pop, shift, unshift,splice, sort, reverse 并不会触发 set)
- 不能监听对象,即必须遍历对象的每个属性。(通常配合
Object.keys
来遍历) - 必须深层遍历嵌套的对象
Proxy
优点:
- 可以监听整个对象,而不是对象的某个属性,但深层对象还是无法监听。
- 支持数组。
- 支持更多的捕捉器
- 会受浏览器厂商更多的关注和性能优化
缺点:
- ES6新语法
- 无法使用 polyfill 来兼容(即兼容性需要考虑)
利用 Proxy
实现类似 Vue 中的数据挟持
javascript
class Observe {
constructor(data) {
this.handle = {
set: (target, property, value) => {
console.log('set:', value)
// 判断新值是否等于旧值
if (target[property] === value) {
return false;
}
target[property] = value;
// TODO 数据变化,通知所有订阅者
return true;
},
get: (target, property) => {
console.log('get:', target[property])
return target[property];
}
}
// 监听每一个数据
this.data = this.defineReactive(data);
}
// 利用proxy监听data,循环遍历监听data下所有对象,并返回proxy
// 注:因为proxy与Object.defineProperty不同,可以监听一个对象,但一个对象的深层对象会监听不到,所有需要遍历监听所有引用类型的。
defineReactive(data) {
if (!data || typeof data !== "object") {
return data;
}
Object.keys(data).forEach(key => {
data[key] = this.defineReactive(data[key]);
});
return new Proxy(data, this.handle);
}
}
const data = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
}
const proxy = new Observe(data).data
console.log(proxy.b.d.e)
proxy.b.d.e = 10
console.log(proxy.b.d.e)
// 输出:
// 第一步操作 console.log(proxy.b.d.e)
// get: { c: 2, d: { e: 3 } }
// get: { e: 3 }
// get: 3
// 3
// 第二步操作 proxy.b.d.e = 10
// get: { c: 2, d: { e: 3 } }
// get: { e: 3 }
// set: 10
// 第三步操作 console.log(proxy.b.d.e)
// get: { c: 2, d: { e: 10 } }
// get: { e: 10 }
// get: 10
// 10