vue2实现响应式的原理(数据驱动视图)是借助了ES5的 Object.defineProperty的get和set,但是它有一些缺陷,比如:
- 一次性要把监听的数据递归到底,效率太低
- 对新增的属性监听不到,无法实现响应式,需要借助Vue.$set
- 对数组的push、pop等操作需要额外的代码才能实现响应式
而Proxy这个新增的API可以完美的解决上面3个问题,在介绍Proxy之前,先来了解一下Vue2是怎么实现数据响应式的。
Object.defineProperty存在的问题
一次性要把监听的数据递归到底,效率太低
const data = {
path: "user",
user: {
name: "mickey",
avator: "xxx.png"
},
list:['1'],
detail:{}
};
function updateView() {
console.log("update view");
}
function observer(data) {
if (typeof data !== "object" || data == null) {
return;
}
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
function defineReactive(data, key, value) {
observer(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newVal) {
// 注意:这里也要深度监听
observer(newVal);
if (value !== newVal) {
updateView();
value = newVal;
}
}
});
}
observer(data);
// 这个代码会在控制台打印一次updateview
data.path = "other";
// 这个代码也会在控制台打印一次updateview,因为在defineReactive函数的第一行实现了深度监听
data.user.name = "mickey_new";
// 会打印updateview
data.detail = {id:1,addr:'xxxx'}
// 会打印updateview,因为在set函数里也调用过了observer实现了深度监听
data.detail.id = 2
这里的代码并不是vue的真实源码,但足以说明vue实现响应式的原理。
值得注意的是,为了实现嵌套对象的响应式,需要在defineReactive和set内部都调用observer
理解了上面的代码就很容易看出来,如果data这个对象特别的复杂、嵌套结构很深,执行完 observer(data) 以后,会递归的把整个数据结构遍历一次,这样才能实现响应式。这就解释了上面说的Object.defineProperty的第一个缺陷,"一次性要把监听的数据递归到底,效率太低"
对新增的属性监听不到,无法实现响应式,需要借助Vue.$set
还是上面的代码,这个时候在data上面添加一个新属性
// 是不会在控制台打印updateview,因为在遍历data的时候的时候newProp属性还没有
data.newProp = 'new'
为了解决这个问题,可以模拟一个类似Vue.$set的方法来解决
function $set(target,propertyName,value){
defineReactive(target,propertyName,value)
}
为了让新增的属性也实现响应式,调用一下$set
$set(data,'newProp')
// 这个时候可以打印 updateview了
data.newProp = 'new'
对数组的push、pop等操作需要额外的代码才能实现响应式
依旧是上面的代码,这个时候调用数组的push方法,不会触发响应式更新
// 不会打印updateview
data.list.push('2')
我们需要改写数组原生的方法
const originArrProto = Array.prorotype;
const arrProto = Object.create(originArrProto);
['push','pop','shift','unshift','slice','splice'].forEach(methodName => {
arrProto[methodName] = () => {
updateView()
originArrProto[methodName].call(this,...arguments);
}
});
// 然后修改 observer 函数,添加对数据的判断
function observer(data) {
if (typeof data !== "object" || data == null) {
return;
}
if (Array.isArray(data)){
data.__proto__ = arrProto;
}
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
为了不污染全局的Array,上面的代码在 observer 函数中增加了判断,这样全局使用数组原生的push、pop等方法不受影响
上面这就是使用Object.defineProperty的三个缺陷的代码演示(只为了说明问题,具体实现可能和vue有出入),下面来看看用Proxy怎么解决这3个问题
Proxy实现响应式
Proxy是ES6+新增的一个API,通过代理来监听对数据的操作,具体的API可以自行搜索。为了实现和上述代码一样的功能(对象深度监听、对新增属性监听、对数据的原生方法监听),我们用Proxy很容易解决。
function reactive(data) {
if (typeof data !== "object" || data == null) {
return data;
}
const proxyConfig = {
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
updateView();
},
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 这个地方深度监听
return reactive(res);
}
};
return new Proxy(data, proxyConfig);
}
function updateView() {
console.log("update view");
}
const data = {
path: "user",
user: {
name: "mickey",
avator: "xxx.png"
},
list: ["1"],
detail: {}
};
const proxyData = reactive(data);
proxyData.path = "other";
proxyData.user.name = "mickey_new";
proxyData.detail = { id: 1, addr: "xxxx" };
proxyData.detail.id = 2;
proxyData.newProp = "new";
proxyData.list.push("2");
上面对proxyData的修改都会打印update view
Proxy本身就可以察觉到新增属性和对数组的原生方法的调用,所以无需额外的代码就可以实现响应式。但是对于深度监听(也就是嵌套对象的监听),我们是在 get 方法里递归调用了 reactive 方法。这里需要强调的是,和vue2使用Object.defineProperty不同,Object.defineProperty是在一上来遍历整个数据结构来实现深度监听,这里用Proxy是在get的时候(访问属性时)才动态的去深度监听,所以Proxy在深度监听性能更好
所以,Proxy这个API可以完美的取代Object.defineProperty