1. Object.defineProperty真的无法监测数组下标的变化吗?
2. 分析vue2.x中对数组Observe部分源码
3. 对比Object.defineProperty和Proxy
![a6fb072cef07f9c3210bf0432cabb8cb.png](https://i-blog.csdnimg.cn/blog_migrate/5d6f6d82bd36ad75c16869cc3c4ac87f.png)
无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。所以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. 通过下标获取某个元素和修改某个元素的值
![7d21c1ceb1bf5fcd47d289e99df8f5fa.png](https://i-blog.csdnimg.cn/blog_migrate/8cf219f8106963c6bf6413a75ad30bf8.png)
![67dd08fccdf5e718a3ad4f24eda60209.png](https://i-blog.csdnimg.cn/blog_migrate/cff620038896be24cfa24cd0c91f9fe4.png)
![1ea2e49d5e2ffca8e3a474b06e16ef8f.png](https://i-blog.csdnimg.cn/blog_migrate/f5eabefcc20e72701aca288f70535c5a.png)
![69719ede63346ce86b89b7fa7da66ddd.png](https://i-blog.csdnimg.cn/blog_migrate/c65565ca1e85f6746ddec2033a14c6f1.png)
![58bc67839fc4412cb7cb2dbf8be984be.png](https://i-blog.csdnimg.cn/blog_migrate/e4d62e7c65d05396739585c9f1d15d21.png)
![c661694cc2d10889ce389a08a2a8bf26.png](https://i-blog.csdnimg.cn/blog_migrate/82fade70dfae83fa0fc8407fbc1c06f0.png)
![bff70942e9f53889dea00607e17a45a9.png](https://i-blog.csdnimg.cn/blog_migrate/74ecb1f68377b3867e5ca0acb6e1ee77.jpeg)
![9c82e1e7bd372b4acd5e3193b1010b95.png](https://i-blog.csdnimg.cn/blog_migrate/9808aab8214d6a065f542094e0c8c8c2.png)
![f55b2e4e80ae74ce4ebb7e8d59892785.png](https://i-blog.csdnimg.cn/blog_migrate/47c5f22339b321fe44a5f016d7079ea5.png)
/* * 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 VS Proxy
上面已经知道
Object.defineProperty
对数组和对象的表现是一致的,那么它和
Proxy
对比存在哪些优缺点呢?
1. Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
由于
Object.defineProperty
只能对属性进行劫持,需要遍历对象的每个属性,
如果属性值也是对象,则需要深度遍历。而
Proxy 直接代理对象,不需要遍历操作
。
2. Object.defineProperty对新增属性需要手动进行Observe。
由于
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 | 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)
拦截对象属性的设置,是可以拦截到对象的新增属性的。
![90199f71be12742357b194ad66956ec5.png](https://i-blog.csdnimg.cn/blog_migrate/2b3a8ec0f6f6a966505144897b86eb65.png)
![b43c174d848829e799d9926122131413.png](https://i-blog.csdnimg.cn/blog_migrate/735e9d253938e2757659cf853073768d.png)
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)。
![fa14f69c24f36ea1fdd6fa134259703a.png](https://i-blog.csdnimg.cn/blog_migrate/313aa719fb11b5fc35d3103c69674038.jpeg)
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
https://segmentfault.com/a/1190000015783546
https://zhuanlan.zhihu.com/p/35080324
http://es6.ruanyifeng.com/#docs/proxy
推荐阅读
我的公众号能带来什么价值?(文末有送书规则,一定要看) 每个前端工程师都应该了解的图片知识(长文建议收藏) 为什么现在面试总是面试造火箭?