从 defineProperty 到 Proxy
众所周知,Vue 2.x 的数据绑定是通过 defineProperty。而在 Vue 3.x 的设计中,数据绑定是通过 Proxy 实现的,这两者到底有何异同?
一、definePropety
defineProperty 是 Object 的一个方法,可以在对象上新增或编辑某个属性,可编辑的内容除了属性值 value 之外,还有该属性的描述信息
Object.defineProperty(obj, prop, descriptor)
该方法接收三个参数,分别是目标对象 obj,被编辑的属性名 prop,以及该属性的描述 descriptor
需要注意的是,只能在 Object 构造器对象使用该方法,实例化的 object 类型是没有该方法的
1. 基础描述符
- configurable: 当该键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
当该描述符为 false 的时候,其它的描述符一旦定义,就无法再更改,且该属性无法被 delete 删除
- enumerable: 当该键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false。
当 enumerable 为 false 时,Objcet.keys() 和 for...in 都无法获取到被定义的属性
但 Reflect.ownKeys() 可以...
2. 数据描述符
- value: 属性值。可以是任何有效的 JavaScript 值 (数值,对象,函数等)。默认为 undefined。
- writable: 当该键值为 true 时,属性的值(即 value)才能被赋值运算符改变。 默认为 false。
3. 存取描述符
- get:该属性的 getter 函数,访问该属性时候会调用该函数,其返回值会被用作 value,默认为 undefined。
该函数没有入参,但是可以使用 this 对象,只是这个 this 不一定是源对象 obj
- set: 该属性的 setter 函数,当属性值被修改时,会调用此函数,默认为 undefined。
该方法接受一个参数,即被赋予的新值,同时会传入赋值时的 this 对象
⚠️注意:数据描述符和存取描述符不可同时存在!
4. Vue 2.x 响应式原理
在 Vue 2.x 中其实就是在观察者模式中使用上面提到的 get 和 set 实现的数据绑定
首先实现依赖收集和 Watcher
// 通过 Dep 解耦属性的依赖和更新操作
class Dep {
constructor() {
this.subs = []
}
// 添加依赖
addSub(sub) {
this.subs.push(sub)
}
// 更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
class Watcher {
constructor(obj, key, up) {
// 手动触发 getter 以添加监听
Dep.target = this
this.up = up
this.obj = obj
this.key = key
this.value = obj[key]
// 完成依赖添加后重置 target
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.up(this.value)
}
}
然后通过 defineProperty 来实现响应
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set(newVal) {
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}
完成之后,通过 observe 遍历对象,然后实例化 Watcher,手动触发一次 getter 完成数据绑定
const data = { name: '' }
observe(data)
function update(value) {
document.body.innerHTML = `<div>${value}</div>`
}
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
data.name = 'Wise.Wrong'
这部分代码参考自掘金小册《前端面试之道》
二、Proxy
以 Object.defineProperty() 实现的响应式有两个问题:
1. 给对象新增属性并不会更新 DOM;
2. 以索引的方式修改数组也不会触发 DOM 的更新。
最终 Vue 是通过重写函数的方式解决了这两个问题,但对于数组的数据绑定依然有瑕疵
而这些问题,对于 Proxy 来说都不是问题
1. 简介
const p = new Proxy(target, handler)
这里的目标对象 target 可以是任何类型的对象,包括原生数组,函数,甚至另一个 Proxy
而对应的处理器对象 handler 包含很多的 trap 方法,这些 trap 方法会在 Proxy 对象执行对应操作时触发
下面会介绍几个常用的方法
getPrototypeOf() | Object.getPrototypeOf 方法对应的钩子函数 |
setPrototypeOf() | Object.setPrototypeOf 方法对应的钩子函数 |
defineProperty() | Object.defineProperty 方法对应的钩子函数 |
has() | in 操作符对应的钩子函数 |
deleteProperty() | delete 操作符对应的钩子函数 |
apply() | 函数被调用时的钩子函数 |
construct() | new 操作符对应的钩子函数 |
get() | 属性读取操作的钩子函数 |
set() | 属性被修改时的钩子函数 |
钩子函数会在对 Proxy 对象执行相应操作的时候触发
2. 钩子函数
以 set 和 get 为例
function update(value = 'wise.wrong') {
console.log('update');
document.body.innerHTML = value;
};
const data = ['who', 'am', 'i'];
const subject = new Proxy(data, {
get: function(obj, prop) {
return obj[prop];
},
set: function(obj, prop, value) {
update(value);
obj[prop] = value;
}
});
上面的目标的对象是一个数组,然后实例化 Proxy 的时候添加了 set 的钩子函数
当 Proxy 对象 subject 被修改的时候,会执行 update 方法
基于这些钩子函数,就可以参考上面 Object.defineProperty() 的思路实现数据绑定了,而且还不会有上面的遗留问题
3. 和 defineProperty 的区别
defineProperty 需要针对具体的 key 设置 getter 和 setter
Object.defineProperty(obj, prop, descriptor)
以至于 Vue 2.x 在初始化的时候,需要递归遍历对象的子属性,挨个儿挂载 setter
这也导致了无法直接通过 defineProperty 实现在对象中新增属性时更新 DOM
但 Proxy 是针对整个对象的代理,不会关心具体的 key
而且 Proxy 的目标对象并没有类型限制,除了 Object 之外,还天然支持 Array、Function 的代理
此外 Proxy 还不仅仅支持 getter 和 setter,上面提到的钩子函数 ,在特定的场景下会发挥出应有的作用
所以 Proxy 比 Object.defineProperty() 的层次更高,毕竟 defineProperty 只是一个方法,而 Proxy 是一个可实例化的类