为什么Vue3.0 不再使用defineProperty实现数据监听

其实这个问题很多文章都有写,也是面试的高频题目,这里仅仅是记录下自己的理解。

ProxyObject.defineproperty的区别

  1. Object.defineProperty只能劫持对象的属性,对于嵌套的对象还需要进行深度的遍历;而Proxy是直接代理整个对象
  2. Object.defineProperty对新增的属性需要手动的Observe(使用$set);Proxy可以拦截到对象新增的属性,数组的pushshiftsplice也能拦截到
  3. Proxy具有13种拦截操作,这是defineProperty不具有的
  4. Proxy 兼容性差 IE浏览器不支持很多种Proxy的方法 目前还没有完整的polyfill方案

defineProperty写法;

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
     get: function defineGet() {
      console.log(`get key: ${key} value: ${value}`)
      return value
    },
     set: function defineSet(newVal) {
      console.log(`set key: ${key} value: ${newVal}`)
      value = newVal
    }
  })
}
function observe(data) {
  Object.keys(data).forEach(function(key) {
    // 递归的getter setter
    defineReactive(data, key, data[key])
  })
}

Proxy的写法:

let proxyObj = new Proxy(data, {
    get(key) {
        return data[key]
    },
    set(key, value) {
        data[key] = value
    }
})

当然还有其他的属性,这里写最简单的。

这两个方法的区别让我想到了事件代理

  • 111
  • 222
  • 333
  • 444

如果没有使用事件代理,那么它会给ul下的每个li绑定事件,这样写有个问题就是,新增的li是没有事件的,事件没有一起添加进去。
如果是使用事件代理,那么新添加的子节点也会有事件响应,因为它是通过触发代理节点(父节点 冒泡)来触发事件的
非常类似,这里想要说明的是:defineProperty是在本身自己的对象属性上做getter/setter, 而Proxy返回的是一个代理对象,只有修改代理对象才会发生响应式,如果修改原来的对象属性,并不会产生响应式更新.

Object.defineProperty对数组的处理

查阅vue官方文档 我们能看到:

Vue 不能检测以下数组的变动:

1、当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
2、当你修改数组的长度时,例如:vm.items.length = newLength

对于第一点:
有一些文章直接写

Object.defineProperty有一个缺陷是无法监听到数组的变化,导致直接通过数组的下标给数组设置值,不能实时响应

这种说法是错误的,事实上Object.defineProperty是可以监听到数组下标的变化,只是在Vue的实现中,从性能/体验的性价比考虑,放弃了这个特性.
对于数组下的索引是可以用getter/setter 的,

image.png

但是vue为什么没这么做?如果监听索引值,通过pushunshift添加进来的元素的索引还没被劫持,也不是响应式的,需要手动的进行observe,通过popshift删除元素,会删除并更新索引,也能触发响应式,但是数组经常会被遍历,会触发很多次索引的getter 性能不是很好。

对于第二点:
MDN:

数组的 length 属性重定义是可能的,但是会受到一般的重定义限制。(length 属性初始为 non-configurable,non-enumerable 以及 writable。对于一个内容不变的数组,改变其 length 属性的值或者使它变为 non-writable 是可能的。但是改变其可枚举性和可配置性或者当它是 non-writable 时尝试改变它的值或是可写性,这两者都是不允许的。)然而,并不是所有的浏览器都允许 Array.length 的重定义。

image.png

image.png

所以对于数组的length,无法对它的访问器属性进行getset,所以没法进行响应式的更新.

这里注意下有两个概念:索引 和 下标
数组有下标,但是对应的下标可能没有索引值!

arr = [1,2]
arr.length = 5
arr[4] // empty 下标为4,值为empty,索引值不存在。 for..in 不会遍历出索引值不存在的元素

手动赋值length为一个更大的值,此时长度会更新,但是对应的索引不会被赋值,也就是对象的属性没有,defineProperty无法处理对未知属性的监听,举个例子:length = 5的数组,未必索引就有4,这个索引(属性)不存在,就没法setter了。

数组的索引跟对象的键表现其实是一致的.

vue对数组进行了单独处理, 对其进行劫持重写,
看一个数组劫持的demo:

const arrayProto = Array.prototype

// 以arrayProto为原型的空对象
const arrayMethods = Object.create(arrayProto)

const methodToPatch = ['push', 'splice']

methodToPatch.forEach(function (method) {
    const original = arrayProto[method]
    
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args)
        console.log('劫持hh')
        return result
    })
})

function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        configurable: true,
        writable: true
    })
}

let arr = [1,2,3]
arr.__proto__ = arrayMethods

arr.push(4)
// 输出
// 劫持hh
// 4

我们以数组为原型创建了一个空对象arrayMethods, 并在其上面定义了要劫持的数组,我们这个只是简单的打印了一句。改变arr的原型指向(给__proto__赋值),在arr操作push,splice时会走劫持的方法。 vue的数组劫持实际上是在劫持方法里面添加了响应式的逻辑.

function mutator(...args) {
    // cache original method
  const original = arrayProto[method]
  // obj key, val, enumerable
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        //eg: push(a) inserted = [a] // 为push的值添加Oberserve响应监听
        inserted = args
        break
      case 'splice':
        // eg: splice(start,deleteCount,...items)  inserted = [items] //  为新添加的值添加Oberserve响应监听
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
}
/**
 * Observe a list of Array items.
 */
observeArray (items: Array) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

$set 手动添加响应式 原理

对于对象新增属性/数组新增元素,无法触发响应式,我们可以用vue $set进行处理

vm.$set(obj,key,value)

对于数组还能使用splice方法:

vm.items.splice(indexOfItem, 1, newValue)

但是它们本质是一样的!

set的实现核心就是:

  1. 如果是数组,会使用splice对元素进行手动observe
  2. 如果是对象
    如果是修改存在的key,直接赋值就会触发响应式更新
    如果是新增的key, 就对key进行手动observe
  3. 如果不是响应式的对象(响应式对象有__ob__ 属性) 就直接赋值

set的内部实现:

export function set (target: Array | Object, key: any, val: any): any {
  // 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息
  // 这个api本来就是给对象与数组使用的
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 类似$vm.set(vm.$data.arr, 0, 3)
    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的splice变异方法触发响应式, 这个前面讲过
    target.splice(key, 1, val)
    return val
  }
  // target为对象, key在target或者target.prototype上。
  // 同时必须不能在 Object.prototype 上
  // 直接修改即可, 有兴趣可以看issue: https://github.com/vuejs/vue/issues/6845
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__
  // Vue 实例对象拥有 _isVue 属性, 即不允许给Vue 实例对象添加属性
  // 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // target本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // ---->进行响应式处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

参考:
https://www.zhihu.com/questio...
https://www.javascriptc.com/3...
https://juejin.cn/post/684490...

你可能感兴趣的:(为什么Vue3.0 不再使用defineProperty实现数据监听)