从 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 是一个可实例化的类
上一篇: js自定义的可分类可搜索的下拉框组件
下一篇: Zookeeper入门实战