在上一篇中,主要讲了Object类型的变化侦测,那么为何要讲Array类型呢?
举个栗子:this.array.push('ntyang')
请各位思考一下,Object类型的变化侦测是经过Object.defineProperty方法的getter/setter实现的,可是使用Array.prototype.push来改变数组,并不会触发getter/setter.那么Array类型数据是如何实现变化侦测的?
接下来让我给您说道说道数组
上面的push
方法,是原型链上的方法,既然没有为数组提供相似Object.defineProperty
的方法,那么咱们换个角度想想,是否是能够从数组的原型链方法做为切入点.
用一个拦截器覆盖Array.prototype
,以后当咱们每次使用数组原型方法时,实际上执行的都是拦截器中的方法,再在拦截器中使用原生的Array.prototype方法.这样,咱们就能够在拦截器中监听到Array的变化了.
Array原型中能够改变自身的方法有七个:push,pop,shift,unshift,splice,sort,reverse
想到这里,那就实现一下:浏览器
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'] methodsToPatch.forEach(method => { const original = arrayProto[method] def(arrayMethods, method, function mutator () { let args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; return original.apply(this, args) }) }) function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); }
咱们用变量arrayPrototype
保存Array.prototype
,具有其全部功能.接着在arrayMethods
上使用Object.defineProperty
对那七个方法进行封装.若是咱们使用一个骚操做,用arrayMethods
覆盖掉Array.prototype
,那么,当咱们使用如push
等方法时,其实调用的是arrayMethods.push
,也就是mutator
函数.而mutator
函数最终会执行数组的原生方法.所以咱们就能够在mutator
函数中搞一些事情,好比发送通知.
那么如何覆盖掉Array.prototype
呢?app
想让拦截器生效就必需要覆盖Array.prototype
.可是若是直接覆盖的话,就污染了全局Array
.我只是想拦截那些响应式的数据啊.
对了,是否是想起上一章的Obverser了?函数
function Observer(value) { this.value = value if(!Array.isArray(value)){ this.walk(value) } else { value.__proto__ = arrayMethods //新增 } }
若是当前value
是Array
类型,直接赋值给它的隐式原型__proto__
可是呢,鉴于目前浏览器对ES6的支持,咱们须要处理不能使用__proto__
的状况.
Vue的处理也是简单直接,既然不能操做你的原型,那就直接操做你吧,毕竟只要自身存在对应的方法就不会再去原型上寻找,即把arrayMethods
中的方法设置到数组上.
修改一下Observe:工具
const hasProto = '__proto__' in {} const arrayKeys = Object.getOwnPropertyNames(arrayMethods); function Observer(value) { this.value = value if(!Array.isArray(value)){ this.walk(value) } else { //新增 if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value,arrayMethods, arrayKeys) } } } function protoAugment (target, src) { target.__proto__ = src } function copyAugment (target, src, keys) { for (let i = 0, l = keys.length; i < l; i ++) { const key = keys[i] def(target, key, src[key]) } }
用hasProto
来判断浏览器是否支持__proto__
,若支持,调用protoAugment
,直接将arrayMethods
添加到要监测的数组原型上;若不支持,则调用copyAugment
,将arrayMethods
设置到数组上.
通过这样一番操做,无论您是啥浏览器,都能被安排的明明白白.开发工具
如今,拦截器有了,只能作到当数组的内容发生变化时去通知.可是通知谁呢?
参考Object的,在getter中收集依赖(watcher),在setter中通知依赖.
其实Array的依赖收集也是在getter中.
这下你可能就有点懵了,别方,让咱们捋一下.
试想一下你在data中定义了一个Array:this
myArray:[1,3,5,7]
在组件中,你会如何使用它呢?确定会是this.myArray
的方式吧,这样确定会触发名字叫作myArray
属性的getter方法,因此,关注一下重点:spa
function defineReactive(data, key, val) { if(typeof val === 'object'){ new Observer(val) } const dep = new Dep() Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { if (Dep.target) { dep.depend() //在这里收集Array的依赖 } return val }, set: function (newVal) { if (val === newVal) return val = newVal dep.notify() } }) }
Array在getter中收集依赖,在拦截器中触发依赖
接下来就要思考,数组的依赖要保存到哪里?
Vue把数组的依赖保存到Observer
中了,由于getter和拦截器中都能访问的到Observer的实例,下面会详细解释prototype
function Observer(value) { this.value = value this.dep = new Dep() //新增 if(!Array.isArray(value)){ this.walk(value) } else { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value,arrayMethods, arrayKeys) } } }
首先收集依赖,那就要改造一下defineReactive
方法:code
function defineReactive(data, key, val) { if(typeof val === 'object'){ new Observer(val) } const dep = new Dep() const childOb = observe(val) //新增 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { if (Dep.target) { dep.depend() if(childOb){ //新增 childOb.dep.depend() } } return val }, set: function (newVal) { if (val === newVal) return val = newVal dep.notify() } }) } function observe(value, asRootData){ if(!isObject(value)){ return } let ob if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer){ ob = value.__ob__ } else { ob = new Observer(value) } return ob } function isObject (obj) { return obj !== null && typeof obj === 'object' } const hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) }
新增一个observe
函数,尝试建立或返回一个Observer实例.若是value已是响应式数据,直接返回Observer实例,避免重复侦测
在defineReactive
函数中,调用observe(val)
,获得当前val对应的Observer实例childOb,最后经过childOb.dep执行depend方法收集依赖.
可能你仍是不太明白为何要把依赖收集到Observer中,继续往下看
如今,咱们已经实现了在getter中把数组的依赖收集到Observer实例中了,那么如何获取Observer实例呢?
再改造一个Observer:
function Observer(value) { this.value = value this.dep = new Dep() def(value, '__ob__', this) //新增 if(!Array.isArray(value)){ this.walk(value) } else { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value,arrayMethods, arrayKeys) } } }
给value设置一个__ob__属性,值是Observer的实例
这样,就能够经过数组的__ob__属性获得Observer实例了,而后就能拿到Dep实例了.
__ob__的做用不仅这些,好比它能够标识当前数据是否是响应式的,有心的你必定会在开发工具中看到响应式的数据都有一个__ob__属性,好比这样:
再让咱们回到拦截器方法,既然能够拿到Observer实例,那就要用起来:
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'] methodsToPatch.forEach(method => { const original = arrayProto[method] def(arrayMethods, method, function mutator () { let args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; const result = original.apply(this, args) const ob = this.__ob__ //新增 ob.dep.notify() //新增 return result }) })
不论是覆盖value的原型链仍是直接将方法添加到value上,均可以经过this.__ob__获取到Observer实例,而后就能够愉快的ob.dep.notify()
啦
以上说的都是侦测数组自身的变化,接下来就要对数组内的每个元素进行侦测
改造一下Observer,当value是数组是,要递归每个元素:
function Observer(value) { this.value = value this.dep = new Dep() def(value, '__ob__', this) if(!Array.isArray(value)){ this.walk(value) } else { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value,arrayMethods, arrayKeys) } this.observeArray(value) //新增 } } Observer.prototype.observeArray = function (items) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
新增observeArray
方法,对value的每一项都执行observe函数,observe函数内会new Observer()
,以后再进入value是Object/Array的判断,递归全部层次.
这样,只要将Array类型的数据丢给Observer,就会把数组的全部子元素都转换成响应式.
好像还忽略了一点东西,对的,就是经过push,unshift和splice添加的数据
也须要转换成响应式的.再修改一下拦截器方法:
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'] methodsToPatch.forEach(method => { const original = arrayProto[method] def(arrayMethods, method, function mutator () { let args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; const result = original.apply(this, args) const ob = this.__ob__ let instered //新增 switch (method) { case 'push': case 'unshift': instered = args break case 'splice': instered = args.slice[2] break } if (instered) ob.observeArray(instered) //新增 ob.dep.notify() return result }) })
对method进行switch判断,把方法中的参数保存到inserted中,若是inserted有值,就用ob.observeArray来侦测一下
因为是经过拦截器的方式实现的数组类型数据的变化侦测,那么就表示必定会有一些操做是Vue拦截不到的,好比:
this.myArray[0] = 123 this.myArray.length = 0
以上这两种操做,Vue彻底侦测不到,也就没法触发watch或re-render操做,页面也就不会有更新,请看官注意一下,除非您想故意这样...