从 defineProperty 到 Proxy

时间:2020-04-14 21:10:00 来源:互联网 作者: 神秘的大神 字体:

众所周知,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 是一个可实例化的类