proxy
在上节课程中,我们建立了一个与vue响应式实现相类似的响应式系统。Object.defineProperty()的使用将属性转化为getter, setter方法,这样能够持续追踪依赖,在属性修改的时候能重新运行匿名函数代码。 如果你有关注过vue的设计蓝图,你会发现vue 2.x 以上的版本用proxy重写过,与我们上节课所展示的不同。 我在这里问了evan (尤雨溪 vue 创始人), 使用proxy重写到底是怎样实现的,重写的好处又在哪里呢?
优点
proxy API 让我们能为对象创建一个虚拟的代理,它提供了一些处理器,比如get(), set(), deleteProperty() 等。当对象上的属性被获取或者是修改的时候,这些处理器起到一个拦截的作用。这将解除以下限制:
- 必须使用Vue.
s
e
t
(
)
来
添
加
响
应
式
的
属
性
,
使
用
V
u
e
.
set()来添加响应式的属性,使用Vue.
set()来添加响应式的属性,使用Vue.delete()删除已有的属性。
- 数组改变的监测。
之前的代码
先前,当属性被获取或者被改变的时,我们使用Object.defineProperty()来监听,如下所示:
let data = { price: 5, quantity: 2 };
let target = null;
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if (target && !this.subscribers.includes(target)) {
this.subscribers.push(target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
Object.keys(data).forEach(key => {
let internalValue = data[key];
const dep = new Dep();
Object.defineProperty(data, key, {
get() {
dep.depend();
return internalValue;
},
set(newVal) {
internalValue = newVal;
dep.notify();
}
});
});
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
watcher(() => {
data.total = data.price * data.quantity;
});
console.log("total = " + data.total)
data.price = 20
console.log("total = " + data.total)
data.quantity = 10
console.log("total = " + data.total)
改进方案:使用proxy重写
摈弃之前循环每个属性,添加getter, setter的写法,我们为data对象创建一个代理,如下所示:
const observedData = new Proxy(data, {
get() {
},
set() {
},
deleteProperty() {
}
});
proxy构造函数的第二个参数叫做处理器。处理器是一个包含有许多捕获器(trap)的对象,当data中的属性发生一些操作(比如被获取,被修改,被删除)时,这些捕获器会进行拦截。 get(), set()方法中分别执行dep 实例的depend, notify方法。甚至对data中新增的属性,set()方法也会被触发。这使得新增的属性也是响应式的,因此我们不再需要使用Vue.$set()来新增响应式的属性。同样的,deleteProperty()捕获器可以用来删除响应式的属性。
使用proxy建立响应式系统
首先我们需要改变之前写的Object.keys(data).forEach循环。我们需要为每个响应式的属性创建一个dep实例,并将它们保存在一个map对象中:
let deps = new Map();
Object.keys(data).forEach(key => {
deps.set(key, new Dep());
});
现在我们使用proxy来替换Object.defineProperty,依赖类保持不变(dep class):
let data_without_proxy = data;
data = new Proxy(data_without_proxy, {
get(obj, key) {
deps.get(key).depend();
return obj[key];
},
set(obj, key, newVal) {
obj[key] = newVal;
deps.get(key).notify();
return true;
}
});
从以上可知,我们创建了data_without_proxy变量用来保存原始的data,因为之后data会被重新赋值成自己的代理。get(), set()方法在处理器中以属性的形式出现。 get(obj, key):当属性被获取时,执行该方法。它接收两个参数,一个是数据对象本身,一个是具体的属性。与具体属性对应的依赖类(dep class)中的depend方法将会被调用。最后,对应的属性值作为返回值被返回。 set(obj, key, newVal) :前两个参数与上面的get方法一样。第三个参数是传入的将要被修改成为的值。当我们对属性重新赋值的时候,对应依赖类(dep class)的notify方法会被调用。
另一个改动
我们还要对代码做一个小小的改动。将total提取成独立的变量,而不是存放在data中的(之前的做法),因为不需要追踪total的变动,没有其他的变量会随着total的改变而改变。
let total = 0;
watcher(() => {
total = data.price * data.quantity;
});
console.log("total = " + total);
data.price = 20;
console.log("total = " + total);
data.quantity = 10;
console.log("total = " + total);
运行以上代码我们可以看到,当price和quantity发生变化时,total会响应式的变化。
添加新的响应式属性
现在,不用额外声明,我们就可以添加响应式的属性了。这也是我们使用proxy的原因之一。下面我们来尝试一下:
deps.set("discount", new Dep());
data["discount"] = 5;
let salePrice = 0;
watcher(() => {
salePrice = data.price - data.discount;
});
console.log("salePrice = " + salePrice);
data.discount = 7.5;
console.log("salePrice = " + salePrice);
课程总结
本节课程,尤雨溪告诉我们在Vue (v2.6-next)以上的版本中是怎样利用proxy实现响应式系统的。我们学到了:
- 上节课响应式系统的局限性。
- proxy 是怎样工作的。
- 怎样使用proxy来构建一个响应式的系统。
在下节课中,我们将深入vue的源码来看看响应式系统是怎样实现的。
|