前言
在Vue的响应式系统中,组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新,这是单向数据流。众所周知,Vue2中使用Object.defineProperty(数据劫持)实现响应式数据,这个方案的弊端是无法监听对象新增属性和删除属性、不能监听数组的变化。在ES6中出现的Proxy(数据代理)解决了Object.defineProperty的问题,它可以做到更好的监听对对象的操作,同时结合Refelct对象,可以提供更好的实现响应式数据方案。
一、Proxy基本实现响应式数据
Vue3采用Proxy实现响应式数据。Proxy可以在目标对象之前架设一层“拦截”,外界对该对象的访问(比如获取属性的值、对属性赋值等等),都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。使用Proxy创建一个对象的代理,对对象的操作通过操作代理对象来完成,而不是操作原来的对象。
// target是目标对象,handler也叫捕获器(trap)
var proxy = new Proxy(target, handler);
- 基本实现
let obj = {
name: 'why',
age: 18
}
let proxyObj = new Proxy(obj, {
get: function (target, key) {
console.log('监听获取:', target[key]);
return target[key]
},
set: function (target, key, value) {
console.log('监听赋值:', value);
// 操作了源对象
target[key] = value
},
deleteProperty: function (target, key) {
delete target[key];
console.log('监听删除:', target[key]);
return true;
}
})
console.log(proxyObj.name); // 获取已有属性
proxyObj.name = 'why1' // 已有属性重新赋值
console.log(proxyObj.name); // 查看已有属性重新赋值
console.log(proxyObj.age); // 获取已有属性
delete proxyObj.age // 删除已有属性
console.log(proxyObj.age); // 查看删除已有属性
console.log(proxyObj.newPro); // 获取不存在的属性
proxyObj.newPro = 'newPro' // 给不存在的属性赋值
console.log(proxyObj.newPro); // 查看新增属性
缺点:操作了源对象。
Proxy捕获器- 一共有
13个,常用的有get/set/has/deleteProperty。 - 用于函数对象的有
construct(拦截Proxy实例作为函数调用的操作,如proxy.call(object, ...args))和apply(拦截Proxy实例作为构造函数调用的操作,如new proxy(...args))。
- 一共有
二、认识Reflect
1.基本概念
Reflect是一个内置对象,译作反射。它提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法,与 proxy handler的方法相同。Reflect的所有属性和方法都是静态的(就像Math对象)。设计目的:
- 早期的规范没有考虑对
对象本身的操作如何设计会更规范,所以将这些API放到了Object上。而Object作为一个构造函数,并不适合这么做。 - 一些类似
in、delete的操作符,看起来奇奇怪怪。 - 所以新增
Reflect,把上面的操作都集中到Relect上。 - 在使用
Proxy时,可以做到不操作对象。
let obj = {
name: 'why',
age: 18
}
if (Reflect.deleteProperty(obj, 'age')) {
console.log('删除成功');
} else {
console.log('删除失败');
}
// 删除成功
一句话总结:堆在Object上的东西太多了,造个新对象来更优雅的使用那些API。用反射这个词挺到位的,功能一样,只是用法不一样。
2.常见方法
和Proxy中的方法一一对应。一共有13个,常用的有get/set/has/deleteProperty。
3.结合Proxy实现响应式数据
使用Reflect的好处:
- 代理对象的目的就是
不直接操作源对象。 Reflect.set返回Boolean值,可以判断本次操作是否成功。- 如果源对象有
setter/getter访问器属性,那么可以通过receiver改变里面的this。Reflect.get和Reflect.set可以设置最后一个receiver参数,指定接收者receiver。receiver就是外层Proxy对象。
let obj = {
_name: 'why',
age: 18,
set name(newValue) {
console.log(this, 'this');
this._name = newValue
},
get name() {
return this._name
}
}
let proxyObj = new Proxy(obj, {
get: function (target, key, receiver) {
console.log('监听获取:', target[key]);
return Reflect.get(target, key, receiver)
},
set: function (target, key, value, receiver) {
console.log('监听赋值:', value);
// const isSuccess = Reflect.set(target, key, value, receiver)
const isSuccess = Reflect.set(target, key, value, { name: 1, age: 2 })
if (!isSuccess) {
throw new Error(`set ${key} failure`)
}
// 使用观察者模式,发布者发布消息,让订阅者更新
// dep.notify()
},
})
console.log(proxyObj.name); // 获取已有属性
proxyObj.name = 'why1' // 已有属性重新赋值
console.log(proxyObj.name); // 查看已有属性重新赋值
一句话总结:Reflect的用法更优雅,功能更全。
三、观察者模式
-
观察者模式:
- 只有
发布者与订阅者,它们之间存在依赖,并且要知道彼此的存在。
- 只有
-
发布者(
Dep):- 需要一个
数组保存所有的订阅者。 - 提供一个
添加订阅者的方法。 - 提供一个
发送通知方法,执行订阅者的更新。
- 需要一个
- 订阅者(
Watcher):- 提供一个
更新方法,供发布者调用。
- 提供一个
// 发布者
class Dep {
constructor() {
this.subs = []
}
// 添加订阅者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach((sub) => {
sub.update()
})
}
}
// 订阅者/观察者
class Watcher {
update() {
console.log('update');
}
}
let dep = new Dep()
let watcher = new Watcher()
// 添加订阅者
dep.addSub(watcher)
// 发布通知
dep.notify()
四、总结
What:
Vue3基于Proxy和Reflect实现响应式数据,结合观察者模式实现响应式。通过Proxy生成一个代理对象,不直接操作源对象而操作代理对象。Reflect对对象的操作比Object更优雅。
Why:
- 可以直接监听对对象的操作而不只是某个属性。
- 代码简洁,不需要遍历所有属性才能达到监听所有属性的目的。有性能提升作用。
- 有13种捕获器,直接起飞。
附录
- 文章:ES6入门-Proxy
- 文章:ES6入门-Reflect