/* * 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<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 。
完美!!!
3. 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] 的操作,返回一个布尔值。