Proxy和defineProperty

1. Object.defineProperty

用于监听对象的数据变化

1.1 语法

Object.defineproperty(obj, key, descriptor)

let obj = {
    age: 11
}
let value = 'xiaoxiao';

Object.defineproperty(obj, 'name', {
    get() {
        return value;
    },
    set(newValue) {
        value = newValue;
    }
})
obj.name = 'pengpeng';

1.2 优势

  • 兼容性好

1.3 缺点

  • 无法监听数组的变化
  • 必须遍历对象的每个属性
  • 必须深层遍历嵌套的对象

1.3.1 无法监听数组的变化

Vue 把会修改原来数组的方法定义为变异方法。

变异方法例如 push、pop、shift、unshift、splice、sort、reverse 等,是无法触发 set 的。

非变异方法,例如 filter,concat,slice 等,它们都不会修改原始数组,而会返回一个新的数组。

Vue 的做法是把这些变异方法重写来实现监听数组变化。

const aryMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]
const arrayAugmentations = {}
 
aryMethods.forEach((method) => {
    // 这里是原生 Array 的原型方法
    let original = Array.prototype[method]
 
    // 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上
    // 注意:是实例属性而非原型属性
    arrayAugmentations[method] = function () {
        console.log('我被改变啦!')
 
        // 调用对应的原生方法并返回结果
        return original.apply(this, arguments)
    }
})
 
let list = ['a', 'b', 'c']
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log
list.__proto__ = arrayAugmentations;
list.push('d') // 我被改变啦!
 
// 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。
let list2 = ['a', 'b', 'c']
list2.push('d') // 不输出内容

1.3.2 必须遍历对象的每个属性

使用 Object.defineProperty 多数情况下要配合 Object.keys 和遍历,于是就多了一层嵌套。

并且由于遍历的原因,假如对象上的某个属性并不需要“劫持”,但此时依然会对其添加“劫持”。

Object.keys(obj).forEach((key) => {
    Object.defineProperty(obj, key, {
        // ...
    })
})

1.3.3 必须深层遍历嵌套的对象

当一个对象为深层嵌套的时候,必须进行逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。

2. Proxy

可以理解为在被劫持的对象之前加了一层拦截

2.1 语法

let proxy = new Proxy({}, {
    get(obj, prop) {
        return obj[prop];
    },
    set(obj, prop, val) {
        obj[prop] = val;
    }
})

proxy 返回的是一个新对象, 可以通过操作返回新的对象达到目的

2.2 优势

  • Proxy 可以直接监听数组的变化

  • Proxy 可以直接监听对象而非属性

  • Proxy 有 13 种拦截方法,比 Object.defineProperty 要更加丰富的多

    (1)defineProperty API 的局限性最大原因是它只能针对单例属性做监听

    Vue2.x 中的响应式实现正是基于 defineProperty 中的 descriptor,对 data 中的属性做了遍历 + 递归,为每个属性设置了 getter、setter。

    这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是 无法做到 setter 监听的,这是 defineProperty 的局限性

    (2)Proxy API 的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码

    Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

    (3)响应式是惰性的

    在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这无疑会有很大的性能消耗

    在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗

2.3 总结

当使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截

而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截