一、问题发现
在之前对于对象属性的监测中,是用Object.defineProperty通过对象的setter、getter方法来监测对象属性的变化,从而通过相应的依赖关系再去做相关的处理
而对于数组方法来说,也就是操作Array.prototype上的方法并不会触发该属性的setter方法,因为这个属性没有进行赋值操作。
因此,当我们需要对数组值的变化进行监测时,需要对它调用的数组原型方法进行重写,使其能够引起属性的setter/getter方法,从而实现监听
重写数组原型方法指的是修改数组的原型指向,进而重写原型方法,至于新的原型指向哪里,看接下来的源码分析
二、源码分析
对数组常用方法进行重写,主要为以下七个方法
pop、shift、push、unshift 、sort、 splice、 reverse
关于怎么去重写,大致就是通过原型链来拦截对数组的操作,从而监测到操作数组的变化,使用函数劫持的方式重写数组方法
1.监听器Observer
if (Array.isArray(value)) { //1.判断数组
if (hasProto) { // 2.判断是否支持原型链
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys) // 如果没有原型链会走def方法添加__ob__属性
}
this.observeArray(value) //3.监测
} else {
this.walk(value)
}
- 过程 在以上部分Observer代码中,对于数组的操作大概如以下几点
- 判断value数据(假设是arr数组)是否为数组
- 是数组的情况下,判断是否支持原型链
- 在支持原型链的情况下,执行protoAugment函数
2.protoAugment
function protoAugment (target, src: Object) {
target.__proto__ = src // 让目标的原型链指向src
}
该函数作用是:将arr.proto===Array.prototype重新修改为arr.proto===arrayMethods
也就是将监听数组的原型指向为自定义的原型对象arrMethods
3.arrayMethods
//继承原型链arrayMethods.proto === Array.protptype
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push','pop','shift', 'unshift','splice','sort','reverse'
]
methodsToPatch.forEach(function (method) {
//1. 遍历常用的7大方法进行重写方法
const original = arrayProto[method]
//2. 重新定义原型方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 对插入的数据再次进行监测
// observeArray是将数组遍历一遍
ob.dep.notify() // 通知视图更新
return result
})
}
arrayMethods作用就是:
- 将
被监听数组arr调用的方法通过原型链的方式从Array.prototype上拿到。(能继承到Array.prototype本身的属性,也不会影响到数组本身其他属性的使用) - 重新定义被调用的方法,使用def函数(对数组方法引起的数组变化进行监听) 调用def函数传递的参数值中,其中重点关注val,也就是mutator函数
mutator函数中返回值就是我们需要设置的新值,也就是我们想要的数据变价,
具体过程:
- 调用数组方法进而操作数组
- 根据实际调用的数组方法(switch)确定新增数组的数据,并对其有效数据进行再次监测
- 数据变化,通知视图的更新
4.def
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
def函数作用:实际就是Object.defineProperty()
总结: 在vue中对于数组的监测,对数组方法的调用进行原型链重写,通过指向自己定义的数组原型方法,新的原型有着原来Array.prototype上的所有方法(不回影响到数组本身其他属性的使用),来实现对数组变化的监测
但是以上都是对数组方法引起的数组变化进行监测,而像下面这样是不能被监测到变化的,需要其他手段,vm.$set和splice
arr[1] = 1212//通过索引赋值
arr.length = 3;//直接修改长度