导读
Vue3 中,响应式数据部分弃用了 Object.defineProperty
,使用 Proxy
来代替它。本文将介绍这两种数据监听的方式区别,并通过以下方面来分析为什么 Vue3 选择弃用Object.defineProperty
。
Object.defineProperty
和Proxy
基础使用。Object.defineProperty
能否监测数组下标的变化。- 分析 Vue2 中对数组
Observe
部分源码。 - 对比
Object.defineProperty
和Proxy
。
基础使用
1 | let person = { name: 'hxb', age: 21 }; |
能否监测数组下标的变化
测试内容与代码
在一些技术博客上看到过这样一种说法,认为 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
正名。
1 | function defineReactive(data, key, value) { |
上面代码对数组
testArr
的每个属性通过Object.defineProperty
进行劫持,下面我们对数组testArr
进行操作,看看哪些行为会触发数组的getter
和setter
方法。
通过下标获取某个元素和修改某个元素的值
1 | testArr[0]; // get key: 0 value: 1 |
可以看到,通过下标获取某个元素会触发
getter
方法,设置某个值会触发setter
方法。
数组的 push 方法
1 | testArr.push(4); // 4 |
push
并未触发setter
和getter
方法,数组的下标可以看做是对象中的key
,这里push
之后相当于增加了下索引为3
的元素,但是并未对新的下标进行observe
,所以不会触发。
数组的 unshift 方法
我擦,发生了什么?
unshift
操作会导致原来索引为 0,1,2,3
的值发生变化,这就需要将原来索引为 0,1,2,3
的值取出来,然后重新赋值,所以取值的过程触发了 getter
,赋值时触发了 setter
。
下面我们看一下原来的值
只有索引为 0,1,2
的属性才会触发 getter
。
这里我们可以对比对象来看,testArr
数组初始值为 [100, 2, 3, 4]
,即只对索引为 0,1,2
执行了 observe
方法,所以无论后来数组的长度发生怎样的变化,依然只有索引为 0,1,2
的元素发生变化才会触发,其他的新增索引,就相当于对象中新增的属性,需要再手动 observe
才可以。
数组的 pop 方法
当移除的元素为引用为 2
的元素时,会触发 getter
。
删除了索引为 2
的元素后,再去修改或获取它的值时,不会再触发 setter
和 getter
。
这和对象的处理是同样的,数组的索引被删除后,就相当于对象的属性被删除一样,不会再去触发 observe
。
到这里,我们可以简单的总结一下结论。
Object.defineProperty
在数组中的表现和在对象中的表现是一致的,数组的索引就可以看做是对象中的 key
。
- 通过索引访问或设置对应元素的值时,可以触发
getter
和setter
方法。 - 通过
push
或unshift
会增加索引,对于新增加的属性,需要再手动初始化才能被observe
。 - 通过
pop
或shift
删除元素,会删除并更新索引,也会触发setter
和getter
方法。
所以,
Object.defineProperty
是有监控数组下标变化的能力的,只是 Vue2 因为性能问题放弃了这个特性。
1 | 性能问题: |
Vue 对数组的 observe 做了哪些处理
- Vue 的
Observer
类定义在core/observer/index.js
中。
- 可以看到,Vue 的
Observer
对数组做了单独的处理。
hasProto
是判断数组的实例是否有__proto__
属性,如果有__proto__
属性就会执行protoAugment
方法,将arrayMethods
重写到原型上。
arrayMethods
是对数组的方法进行重写,定义在core/observer/array.js
中,下面是这部分源码的分析。
1 | import { def } from '../util/index'; |
Object.defineProperty Vs Proxy
上面已经知道
Object.defineProperty
对数组和对象的表现是一致的,那么它和Proxy
对比存在哪些优缺点呢?
Object.defineProperty 只能劫持对象的属性,而Proxy是直接代理对象。
- 由于
Object.defineProperty
只能对属性进行劫持,需要遍历对象的每个属性。而Proxy
可以直接代理对象。
Object.defineProperty 对新增属性需要手动进行 Observe。
由于 Object.defineProperty
劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty
进行劫持。
也正是因为这个原因,使用 Vue 给 data
中的数组或对象新增属性时,需要使用 vm.$set
才能保证新增的属性也是响应式的。
- 下面看一下 Vue 的
set
方法是如何实现的,set
方法定义在core/observer/index.js
,下面是核心代码。
1 | /** |
在 set
方法中,对 target
是数组和对象做了分别的处理,target
是数组时,会调用重写过的 splice
方法进行手动 Observe
。
对于对象,如果 key
本来就是对象的属性,则直接修改值触发更新,否则调用 defineReactive
方法重新定义响应式对象。
如果采用 Proxy
实现,Proxy
通过 set(target, propKey, value, receiver)
拦截对象属性的设置,是可以拦截到对象的新增属性的。
不止如此,Proxy
对数组的方法也可以监测到,不需要像上面 Vue2 源码中那样进行 hack
。
1 | let obj = { prop1: 1 }; |
Perfect!!!
Proxy 支持 13 种拦截操作,这是 defineProperty 没有的。
操作 | 介绍 |
---|---|
get(target, propKey, receiver) | 拦截对象属性的读取,比如 proxy.foo 和 proxy['foo'] 。 |
set(target, propKey, value, receiver) | 拦截对象属性的设置,比如 proxy.foo = v 或 proxy['foo'] = v ,返回一个布尔值。 |
has(target, propKey) | 拦截 propKey in proxy 的操作,返回一个布尔值。 |
deleteProperty(target, propKey) | 拦截 delete proxy[propKey] 的操作,返回一个布尔值。 |
ownKeys(target) | 拦截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy) 、Object.keys(proxy) 、for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。 |
getOwnPropertyDescriptor(target, propKey) | 拦截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回属性的描述对象。 |
defineProperty(target, propKey, propDesc) | 拦截 Object.defineProperty(proxy, propKey, propDesc) 、Object.defineProperties(proxy, propDescs) ,返回一个布尔值。 |
preventExtensions(target) | 拦截 Object.preventExtensions(proxy) ,返回一个布尔值。 |
getPrototypeOf(target) | 拦截 Object.getPrototypeOf(proxy) ,返回一个对象。 |
isExtensible(target) | 拦截 Object.isExtensible(proxy) ,返回一个布尔值。 |
setPrototypeOf(target, proto) | 拦截 Object.setPrototypeOf(proxy, proto) ,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 |
apply(target, object, args) | 拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args) 、proxy.call(object, ...args) 、proxy.apply(...) 。 |
construct(target, args) | 拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args) 。 |
新标准性能红利
Proxy
作为新标准,长远来看,JS 引擎会继续优化Proxy
,但getter
和setter
基本不会再有针对性优化。
Proxy 兼容性差
可以看到,Proxy
对于 IE 浏览器来说简直是灾难。
并且目前并没有一个完整支持 Proxy
所有拦截方法的 Polyfill 方案,有一个 Google 编写的 proxy-polyfill 也只支持了 get,set,apply,construct
四种拦截,可以支持到 IE9+ 和 Safari 6+。
总结
Object.defineProperty
对数组和对象的表现一致,并非不能监控数组下标的变化,Vue2 中无法通过数组索引来实现响应式数据的自动更新是 Vue 本身的设计导致的,不是defineProperty
的问题。Object.defineProperty
和Proxy
本质差别是,defineProperty
只能对属性进行劫持,新增属性需要手动Observe
的问题。Proxy
作为新标准,浏览器厂商势必会对其进行持续优化,但它的兼容性也是块硬伤,并且目前还没有完整的 polyfill 方案。
参考来源
转改自掘金
参考 MDN defineProperty
参考 MDN Proxy