vue3.0中,响应式数据部分弃用了 Object.defineProperty ,使用 Proxy 来代替它。本文将主要通过以下方面来分析为什么vue选择弃用 Object.defineProperty 。
Object.defineProperty 真的无法监测数组下标的变化吗?
分析vue2.x中对数组 Observe 部分源码
对比 Object.defineProperty 和 Proxy
一、无法监控到数组下标的变化?
在一些技术博客上看到过这样一种说法,认为 Object.defineProperty 有一个缺陷是无法监听数组变化:
无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。所以vue才设置了7个变异数组( push 、 pop 、 shift 、 unshift 、 splice 、 sort 、 reverse )的 hack 方法来解决问题。
Object.defineProperty 的第一个缺陷,无法监听数组变化。 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法, vm.items[indexOfItem] = newValue 这种是无法检测的。
这种说法是有问题的,事实上, Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性。
下面我们通过一个例子来为 Object.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) { defineReactive(data, key, data[key]) }) } let arr = [1, 2, 3] observe(arr)
上面代码对数组arr的每个属性通过 Object.defineProperty 进行劫持,下面我们对数组arr进行操作,看看哪些行为会触发数组的 getter 和 setter 方法。
1. 通过下标获取某个元素和修改某个元素的值
可以看到,通过下标获取某个元素会触发 getter 方法, 设置某个值会触发 setter
方法。
接下来,我们再试一下数组的一些操作方法,看看是否会触发。
2. 数组的 push 方法
push 并未触发 setter 和 getter 方法,数组的下标可以看做是对象中的 key ,这里 push 之后相当于增加了下索引为3的元素,但是并未对新的下标进行 observe ,所以不会触发。
3. 数组的 unshift 方法
我擦,发生了什么?
unshift 操作会导致原来索引为0,1,2,3的值发生变化,这就需要将原来索引为0,1,2,3的值取出来,然后重新赋值,所以取值的过程触发了 getter ,赋值时触发了 setter 。
下面我们尝试通过索引获取一下对应的元素:
只有索引为0,1,2的属性才会触发 getter 。
这里我们可以对比对象来看,arr数组初始值为[1, 2, 3],即只对索引为0,1,2执行了 observe 方法,所以无论后来数组的长度发生怎样的变化,依然只有索引为0,1,2的元素发生变化才会触发,其他的新增索引,就相当于对象中新增的属性,需要再手动 observe 才可以。
4. 数组的 pop 方法
当移除的元素为引用为2的元素时,会触发 getter 。
删除了索引为2的元素后,再去修改或获取它的值时,不会再触发 setter 和 getter 。
这和对象的处理是同样的,数组的索引被删除后,就相当于对象的属性被删除一样,不会再去触发 observe 。
到这里,我们可以简单的总结一下结论。
Object.defineProperty 在数组中的表现和在对象中的表现是一致的,数组的索引就可以看做是对象中的 key 。
通过索引访问或设置对应元素的值时,可以触发 getter 和 setter 方法
通过 push 或 unshift 会增加索引,对于新增加的属性,需要再手动初始化才能被 observe 。
通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。
所以, Object.defineProperty 是有监控数组下标变化的能力的,只是vue2.x放弃了这个特性。
二、vue对数组的observe做了哪些处理?
vue的 Observer 类定义在 core/observer/index.js 中。
可以看到,vue的 Observer 对数组做了单独的处理。
hasProto 是判断数组的实例是否有 __proto__ 属性,如果有 __proto__ 属性就会执行 protoAugment 方法,将 arrayMethods 重写到原型上。 hasProto 定义如下。
arrayMethods 是对数组的方法进行重写,定义在 core/observer/array.js 中, 下面是这部分源码的分析。