vue3.0中,响应式数据部分弃用了 Object.defineProperty
,使用 Proxy
来代替它。本文将主要经过如下方面来分析为何vue选择弃用 Object.defineProperty
。javascript
Object.defineProperty
真的没法监测数组下标的变化吗?Observe
部分源码Object.defineProperty
和 Proxy
在一些技术博客上看到过这样一种说法,认为 Object.defineProperty
有一个缺陷是没法监听数组变化:前端
没法监控到数组下标的变化,致使直接经过数组的下标给数组设置值,不能实时响应。因此vue才设置了7个变异数组(
push
、pop
、shift
、unshift
、splice
、sort
、reverse
)的hack
方法来解决问题。vue
Object.defineProperty
的第一个缺陷,没法监听数组变化。 然而Vue的文档提到了Vue是能够检测到数组变化的,可是只有如下八种方法,vm.items[indexOfItem] = newValue
这种是没法检测的。java
这种说法是有问题的,事实上,Object.defineProperty
自己是能够监控到数组下标的变化的,只是在 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性。git
下面咱们经过一个例子来为 Object.defineProperty
正名:es6
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
方法。github
getter
方法, 设置某个值会触发
setter
方法。
接下来,咱们再试一下数组的一些操做方法,看看是否会触发。segmentfault
push
并未触发 setter
和 getter
方法,数组的下标能够看作是对象中的 key
,这里 push
以后至关于增长了下索引为3的元素,可是并未对新的下标进行 observe
,因此不会触发。数组
我擦,发生了什么?浏览器
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
才能够。
当移除的元素为引用为2的元素时,会触发 getter
。
删除了索引为2的元素后,再去修改或获取它的值时,不会再触发 setter
和 getter
。
这和对象的处理是一样的,数组的索引被删除后,就至关于对象的属性被删除同样,不会再去触发 observe
。
到这里,咱们能够简单的总结一下结论。
Object.defineProperty
在数组中的表现和在对象中的表现是一致的,数组的索引就能够看作是对象中的 key
。
getter
和 setter
方法push
或 unshift
会增长索引,对于新增长的属性,须要再手动初始化才能被 observe
。pop
或 shift
删除元素,会删除并更新索引,也会触发 setter
和 getter
方法。因此,Object.defineProperty
是有监控数组下标变化的能力的,只是vue2.x放弃了这个特性。
vue的 Observer
类定义在 core/observer/index.js
中。
能够看到,vue的 Observer
对数组作了单独的处理。
hasProto
是判断数组的实例是否有 __proto__
属性,若是有 __proto__
属性就会执行 protoAugment
方法,将 arrayMethods
重写到原型上。 hasProto
定义以下。
arrayMethods
是对数组的方法进行重写,定义在 core/observer/array.js
中, 下面是这部分源码的分析。
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */
import { def } from '../util/index'
// 复制数组构造函数的原型,Array.prototype也是一个数组。
const arrayProto = Array.prototype
// 建立对象,对象的__proto__指向arrayProto,因此arrayMethods的__proto__包含数组的全部方法。
export const arrayMethods = Object.create(arrayProto)
// 下面的数组是要进行重写的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/** * Intercept mutating methods and emit events */
// 遍历methodsToPatch数组,对其中的方法进行重写
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// def方法定义在lang.js文件中,是经过object.defineProperty对属性进行从新定义。
// 即在arrayMethods中找到咱们要重写的方法,对其进行从新定义
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
// 上面已经分析过,对于push,unshift会新增索引,因此须要手动observe
case 'push':
case 'unshift':
inserted = args
break
// splice方法,若是传入了第三个参数,也会有新增索引,因此也须要手动observe
case 'splice':
inserted = args.slice(2)
break
}
// push,unshift,splice三个方法触发后,在这里手动observe,其余方法的变动会在当前的索引上进行更新,因此不须要再执行ob.observeArray
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
复制代码
上面已经知道 Object.defineProperty
对数组和对象的表现是一致的,那么它和 Proxy
对比存在哪些优缺点呢?
因为 Object.defineProperty
只能对属性进行劫持,须要遍历对象的每一个属性。而 Proxy
能够直接代理对象。
因为 Object.defineProperty
劫持的是对象的属性,因此新增属性时,须要从新遍历对象,对其新增属性再使用 Object.defineProperty
进行劫持。
也正是由于这个缘由,使用vue给 data
中的数组或对象新增属性时,须要使用 vm.$set
才能保证新增的属性也是响应式的。
下面看一下vue的 set
方法是如何实现的,set
方法定义在 core/observer/index.js
,下面是核心代码。
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */
export function set (target: Array<any> | Object, key: any, val: any): any {
// 若是target是数组,且key是有效的数组索引,会调用数组的splice方法,
// 咱们上面说过,数组的splice方法会被重写,重写的方法中会手动Observe
// 因此vue的set方法,对于数组,就是直接调用重写splice方法
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 对于对象,若是key原本就是对象中的属性,直接修改值就能够触发更新
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// vue的响应式对象中都会添加了__ob__属性,因此能够根据是否有__ob__属性判断是否为响应式对象
const ob = (target: any).__ob__
// 若是不是响应式对象,直接赋值
if (!ob) {
target[key] = val
return val
}
// 调用defineReactive给数据添加了 getter 和 setter,
// 因此vue的set方法,对于响应式的对象,就会调用defineReactive从新定义响应式对象,defineReactive 函数
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
复制代码
在 set
方法中,对 target
是数组和对象作了分别的处理,target
是数组时,会调用重写过的 splice
方法进行手动 Observe
。
对于对象,若是 key
原本就是对象的属性,则直接修改值触发更新,不然调用 defineReactive
方法从新定义响应式对象。
若是采用 proxy
实现,Proxy
经过 set(target, propKey, value, receiver)
拦截对象属性的设置,是能够拦截到对象的新增属性的。
不止如此,Proxy
对数组的方法也能够监测到,不须要像上面vue2.x源码中那样进行 hack
。
完美!!!
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
对于IE浏览器来讲简直是灾难。
而且目前并无一个完整支持 Proxy
全部拦截方法的Polyfill方案,有一个google编写的 proxy-polyfill 也只支持了 get,set,apply,construct 四种拦截,能够支持到IE9+和Safari 6+。
Object.defineProperty
对数组和对象的表现一直,并不是不能监控数组下标的变化,vue2.x中没法经过数组索引来实现响应式数据的自动更新是vue自己的设计致使的,不是 defineProperty
的锅。
Object.defineProperty
和 Proxy
本质差异是,defineProperty
只能对属性进行劫持,新增属性须要手动 Observe
的问题。
Proxy
做为新标准,浏览器厂商势必会对其进行持续优化,但它的兼容性也是块硬伤,而且目前尚未完整的polifill方案。
developer.mozilla.org/zh-CN/docs/…
es6.ruanyifeng.com/#docs/proxy
欢迎关注个人公众号「前端小苑」,我会按期在上面更新原创文章。
![]()