在最新的Vue 3.x
,一个很重要的改变就是将使用 ES6
的Proxy
作为其观察者机制,取代之前使用的Object.defineProperty
。对于Object.defineProperty
大家应该在学习Vue
中的响应式数据时深有体会,它可以 重写属性的 get
与 set
方法,从而完成监听数据的改变。
1. Vue 2.x
中的 Object.defineProperty
实现响应式数据
简单的用input
实现一个v-model
:
vue双向绑定实现
下面我们看一下在vue
中是怎么实现的:
Observer:数据的观察者,让数据对象的读写操作都处于自己的监管之下。当初始化实例的时候,会递归遍历data
,用Object.defineProperty
来拦截数据。
Dep:数据更新的发布者,get
数据的时候,收集订阅者(dep.addSub()
),触发Watcher的依赖收集(this.subs.push(sub)
);set
数据时通知Watcher(dep.notify()
),发布更新(update()
) 。
Watcher:数据更新的订阅者,订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。
大家可以参考我在GitHub上的vue
中MVVM
的具体实现:https://github.com/GitHubzl0212/MVVM。
但是在vue3+
中要用Proxy
取代自然有作者的道理。简单来说Object.defineProperty
有以下几个缺陷:
⑴ 无法检测数组的变化
Object.defineProperty
无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 而且使用这些方法(push
, pop
, shift
, unshift
,splice
, sort
, reverse
…)是不能触发set
的,Vue
中能监听是因为对这些方法进行了重写;
var a = {},
bValue = 1;
Object.defineProperty(a,"b",{
set: function(value){
bValue = value;
console.log("setted");
},
get: function(){
return bValue;
}
});
a.b = []; //setted
a.b = [1,2,3]; //setted
a.b[1] = 10; //无输出
a.b.push(4); //无输出
a.b.length = 5; //无输出
当a.b
被设置为数组后,只要不是重新赋值一个新的数组对象,任何对数组内部的修改都不会触发setter
方法的执行。所以要想实现实现数组的双向绑定,则必须通过Arr = newArr;
这样的语句实现。同样常见的数组方法也不会触发,在框架中对这些方法进行了重写才能实现效果。
⑵ 只能监听属性,而不是监听对象本身,需要对对象的每个属性进行遍历。对于原本不在对象中的属性难以监听。在Vue 2.x
里,是通过 “callback
+ 遍历 data
对象” 来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择,而Proxy
就显示了这方面的优势。
⑶ 当对象增删的时候,是监控不到的。比如:data = {a:"a"}
,这个时候如果我们设置data.test = "test"
,这个时候是监控不到的。因为在observe data
的时候,会遍历已有的每个属性(比如a
),添加getter/setter
,而后面设置的test
属性并没有机会设置getter/setter
,所以检测不到变化。同样的,删除对象属性的时候,getter/setter
会跟着属性一起被删除掉,拦截不到变化。
2. ES6中的Proxy
Proxy
可以理解为“代理”而不是“拦截”的意思,Proxy
可以拦截js
引擎内部目标的底层对象操作,这些底层对象操作被拦截后会触发响应特定操作的陷阱函数,每个代理陷阱对应一个命名和参数一致的Reflect
方法。ES6
中扩展了13个代理陷阱:
⑴ 使用set陷阱验证属性
set
陷阱函数接受四个参数:
下面实现一个属性值时数字的对象,对象中每新增一个属性都要加以验证,如果不是数字就抛出错误。
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
//排除已有的属性
if(!trapTarget.hasOwnProperty(key)) {
if(isNaN(value)) {
throw new TypeError("属性必须是数字");
}
}
//添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
//添加新属性
proxy.count = 1;
console.log(proxy.count); //1
console.log(target.count); //1
//由于目标已有name属性,因而可以给他赋值
proxy.name = "proxy";
console.log(proxy.name); //proxy
console.log(target.name); //proxy
//给不存在的属性赋值会发生错误
proxy.newname = "apple"; //属性必须是数字
⑵ 使用get陷阱验证对象结构
get
陷阱函数接受三个参数:
在大多数语言中,如果target
没有name
属性,尝试读取target.name
会抛出一个错误。但是js
中却用undefined
来代替target.name
的属性的值。而代理可以通过检查对象结构来帮助我们回避这个问题。
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if(!(key in receiver)) {
throw new TypeError("属性" + key + "不存在");
}
return Reflect.get(trapTarget, key, receiver)
}
});
//添加一个属性
proxy.name = "proxy";
console.log(proxy.name); //proxy
//如果属性不存在,则抛出错误
console.log(proxy.nam); //TypeError: 属性nam不存在
⑶ 使用has陷阱隐藏已有属性
可以用in
操作符来检测给定对象中是否含有某个属性,如果自有属性或原型属性匹配这个名称或Symbol
就返回true
,例如:
let target = {
value: 42
};
console.log("value" in target); //true, 自有属性
console.log("toString" in target); //true, 继承自Object的原型属性
在代理中使用has陷阱可以拦截这些in操作并会返回一个不同的值。
has
陷阱函数接受两个参数:
例如,可以同时使用has
陷阱和Reflect.has()
改变一部分属性被in
检测时的行为
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
has(traptarget, key) {
if(key==="value") {
return false
} else {
return Reflect.has(traptarget, key)
}
}
});
console.log("value" in proxy); //false
console.log("name" in proxy); //true
console.log("toString" in proxy); //true
代理中的has陷阱会检查key
是否为“value
”,如果是的话返回false
,若不是则调用Reflect.has()
方法返回默认行为。
参考:
https://segmentfault.com/a/1190000006599500
https://blog.csdn.net/ijarvis/article/details/80485972
《深入理解ES6》