在最新的Vue 3.x,一个很重要的改变就是将使用 ES6的Proxy 作为其观察者机制,取代之前使用的Object.defineProperty。对于Object.defineProperty大家应该在学习Vue中的响应式数据时深有体会,它可以 重写属性的 get 与 set 方法,从而完成监听数据的改变。
1. Vue 2.x中的 Object.defineProperty实现响应式数据
简单的用input实现一个v-model:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vue双向绑定实现</title>
</head>
<body>
<input type="text" id="input">
<span id="span"></span>
<script>
const obj = {};
Object.defineProperty(obj, 'text', {
get: function() {
console.log('get val');
},
set: function(newVal) {
document.getElementById('input').value = newVal;
document.getElementById('span').innerHTML = newVal;
}
});
const input = document.getElementById('input');
input.addEventListener('keyup', function(e){
obj.text = e.target.value;
})
</script>
</body>
</html>
下面我们看一下在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陷阱函数接受四个参数:
- trapTarget 用于接收属性(代理的目标)的对象
- key 要写入的属性键
- value 被写入属性的值
- receiver 操作发生的对象(通常是代理)
下面实现一个属性值时数字的对象,对象中每新增一个属性都要加以验证,如果不是数字就抛出错误。
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陷阱函数接受三个参数:
- trapTarget 被读取的属性的源对象(代理的目标)
- key 要读取的属性键
- receiver 操作发生的对象(通常是代理)
在大多数语言中,如果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陷阱函数接受两个参数:
- trapTarget 读取属性的对象(代理的目标)
- key 要检查的属性键(字符串或Symbol)
例如,可以同时使用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》