在上一篇中,主要讲了Object类型的变化侦测,那么为什么要讲Array类型呢?
举个栗子:this.array.push('ntyang')
请各位思考一下,Object类型的变化侦测是通过Object.defineProperty方法的getter/setter实现的,但是使用Array.prototype.push来改变数组,并不会触发getter/setter.那么Array类型数据是如何实现变化侦测的?
接下来让我给您说道说道
1.如何知道变化了呢?
上面的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呢?
2. 覆盖Array.prototype
想让拦截器生效就必须要覆盖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设置到数组上.
经过这样一番操作,不管您是啥浏览器,都能被安排的明明白白.
3. 依赖怎么收集呢?又是怎么通知呢?
现在,拦截器有了,只能做到当数组的内容发生变化时去通知.但是通知谁呢?
参考Object的,在getter中收集依赖(watcher),在setter中通知依赖.
其实Array的依赖收集也是在getter中.
这下你可能就有点懵了,别方,让我们捋一下.
试想一下你在data中定义了一个Array:
myArray:[1,3,5,7]
在组件中,你会如何使用它呢?肯定会是this.myArray的方式吧,这样肯定会触发名字叫做myArray属性的getter方法,所以,关注一下重点:
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的实例,下面会详细解释
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方法:
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()啦
4.侦测数组内各个元素的变化
以上说的都是侦测数组自身的变化,接下来就要对数组内的每一个元素进行侦测
改造一下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来侦测一下
5.关于Array的问题
由于是通过拦截器的方式实现的数组类型数据的变化侦测,那么就表示一定会有一些操作是Vue拦截不到的,比如:
this.myArray[0] = 123
this.myArray.length = 0
以上这两种操作,Vue完全侦测不到,也就无法触发watch或re-render操作,页面也就不会有更新,请看官注意一下,除非您想故意这样...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。