Array的变化侦测
Array所使用的侦测方法与Object不同,这是因为对于Object数据我们使用的时JS提供的对象原型上的方法Object.defineProperty,而这个方法是对象原型上的,所以Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制,当然设计思路是一样的,根本上还是在获取数据时收集依赖,数据变化时通知以来更新。
在哪里收集
Array型数据的依赖收集方式和Object数据的依赖收集方式相同,都是在getter中收集,虽然Array无法使用Object.defineproperty方法,但是平常在开发的时候,arr一般都是包含在data所返回的对象中的。
data(){
return {
arr:[1,2,3]
}
}
我们说过,谁用到了数据,谁就是依赖,那么要用到arr这个数据,我们就要先从object数据对象中获取一下arr数据,而从object数据对象中获取arr数据自然就会触发arr的getter,所以我们就可以在getter中收集依赖
使Array型数据可观测
目前我们已经完成了一般可观测,即我们只知道Array型数据何时被读取了,而何时发生变化我们无法知道。
Object变化时通过setter来追踪的,只有某个数据发生了变化,就一定会触发这个数据上的setter,但是Array型数据没有setter怎么办?
我们知道,要想让Array型数据发生变化,那必然是操作了Array,而JS中提供的操作数组的方法就那么几种,我们可以重构这些方法,在不改变原有功能的前提下,我们为其新增一些其他功能
let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
console.log('arr被修改了')
this.push(val)
}
arr.newPush(4)
在上面的例子中,我们针对数组的原生push方法定义一个新的newPUsh方法,这个newPush方法内部调用了原生push方法,这样就保证了新的newPush方法跟原生push方法具有相同的功能,而且我们还可以在newPush方法内部干一些别的事情,比如通知变化。
基于以上思路,在VUE中创建一个数组方法拦截器,他拦截在数组实例与Array.prototype之间,在拦截器内部重写操作数组的一些方法,当数组实例使用操作数组的方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。
Array原型中可以改变数组自身内容的方法有7个,分别是:push,pop,shift,unshift,splice,sort,reverse。那么拦截器如下:
//源码位置:/src/core/observer/array.js
const arrayProto = Array.prototyep
export const arrayMethods = Object.create(arrayProto) //以数组原型为原型创建一个新对象,后续将新对象作为拦截器替换原Array原型
const methodsToPathc = {
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
}
/**
* 定义新的方法
*/
methodsToPatch.forEach(function(method){
const original = arrayProto(menthod) //缓存原生方法
Object.property(arrayMethods,method,{ //Object.property的作用不只是可以检测到数据的读取与改变,还可以直接改变对象的值
enumerable: false, //是否可以被枚举,因为是原型方法所以不可以在枚举的时候出现
configurable: true, //是否可以被配置,因为要改变值所以是true
writable: true,//是否可以赋值
value: function mutator(...args){
cosnt result = original.apply(this,args) //执行原生方法并将this指向当前所改变的方法
return result
}
})
})
在上面代码中,首先创建了继承自Array原型的空对象arrayMethos,接着在arrayMethos上使用Object.defineproperty方法将那些可以改变自身的7个数组方法遍历逐个进行封装。最后,当我们使用push方法的时候,其实用的是函数mutator,而mutator函数内部执行了original函数,这个original函数就是Array.defineproperty上对应的原生方法。那么,接下来我们就可以在mutator函数中做一些其他的事,比如说发送通知。
接下来我们只需要把它挂载到数组实例与Array.prototype之间,这样拦截器才能够生效
//源码位置: /src/core/observer/index.js
export class Observer{
constructor (value){
this.value = value
if(Array.isArray(value)){
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
}else{
this.walk(value)
}
}
}
//能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性
export const hasProto = '__proto__' in {}
cosnt arrayKeys = Object.getOwnPropertyNames(arrayMethods) //枚举出对象内所有的key,包括不可枚举的属性
function protoAugment(target,src: Object,keys: any){
target.__proto__ = src
}
function copyAugment(target: Object,src: Object,keys: array<string>){
for(let i = 0, i < keys.length; i++){
cosnt key = keys[i]
def(target,key, src[key])
}
}
上面代码首先判断了浏览器是否支持__proto__,如果支持,则调用protoAugment函数把value.proto = arrayMethods;如果不支持,则调用copyAugment函数把拦截器中重写的7个方法循环加入到value上。
拦截器生效后,当数组数据再发生变化时,我们就可以在拦截器中通知变化了,也就是我们就可以知道数组数据何时发生变化了