这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
vue的变化侦测-Array篇
上次我们说过Object数据的变化侦测。Object的变化侦测主要依赖的是Object原型上的方法Object.defineProperty来监听其中的get和set方法,从而在get中收集依赖,在set中通知依赖更新。
但是Array型的数据是没有Object.defineProperty这个方法的,所以我们无法像Object一样来侦听。
1. array型数据是怎么来进行变化侦测的?
其实变化侦测的机制还是不变的,都是在获取数据时收集依赖,然后数据变化时通知依赖更新。
2. 怎么收集依赖的?
还是在get中收集依赖,谁获取了这个数组,就在get中收集依赖。与对象的依赖收集机制是相同的。
3. 怎么监测到array型数据变化了?(怎么通知依赖更新?)
因为数组是没有Object.defineProperty方法的,所以就不能通过get、set来监测变化。但是数组如果变化了,那么必定是操作了数组,而JavaScript中提供的操作数组的方法就那么几种'push','pop','shift','unshift','splice','sort','reverse'。
vue内部就是通过将这几个操作数组的方法进行重写,在重写的方法中调用原生的数组方法,这样就可以保证重写的方法和原生的方法具有相同的功能。还可以在重写的方法中就可以做一些其他的事情,比如通知依赖更新。
4. 数组方法拦截器
// 源码位置 src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 创建一个对象作为拦截器,Object.create 是创建一个新对象,新对象的__proto__是传入的第一个参数
// arrayMethods 就是数组方法拦截器
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 原生方法
def(arrayMethods, method, function mutator (...args) { // 给arrayMethods 添加一个 method属性,属性的值是后面的function
const result = original.apply(this, args)
const ob = this.__ob__ //Observer类实例
ob.dep.notify()
return result
})
})
5. 这个拦截器是怎么使用的?
使用这个拦截器时很简单,只需要将数组实例的__proto__属性设置为这个拦截器即可,这样数组实例调用原生的方法比如push方法时就会调用arrayMethods 上的push,即我们重写的mutator 方法。
那具体是怎么挂载到__proto__上的,我们来看代码:
// 源码位于 src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) { // 检测浏览器 是否支持 __proto__
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) // 将数组中的所有元素都转化为可监测的响应式数据
} else {
this.walk(value)
}
}
/**
* Object.getOwnPropertyNames
* 返回值:数组类型:包含指定对象的自身拥有的枚举和不可枚举的属性名组成的数组
*/
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
['push', 'splice', '__proto__', 'length']
// 这个方法就是讲数组实例的 __proto__指向了arrayMethods
function protoAugment (target, src: Object) {
target.__proto__ = src
}
//如果浏览器不支持__protp__,则调用此方法,将拦截器中重写的那几个方法循环加入到数组实例上
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
6. 深度侦测
在前文所有讲的Array型数据的变化侦测都仅仅说的是数组自身变化的侦测,比如给数组新增一个元素或删除数组中一个元素,而在Vue中,不论是Object型数据还是Array型数据所实现的数据变化侦测都是深度侦测,所谓深度侦测就是不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化。
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 将数组中的所有元素都转化为可被侦测的响应式
} else {
this.walk(value)
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
7. 新增数组元素的侦测
对于数组 中已有的元素我们已经可以进行侦测了,但是如果数组元素中新push了一条数据,即新增了一条数据,我们也应该把这个新增的元素变成可侦测的。 这个实现也比较简单,我们只需要拿到新增的数据,然后调用Observe函数,将其转化为可侦测的就行。
向数组中新增元素的方法有3个,push、unshift、splice.我们可以通过监听这个三个方法,拿到新增的数据即可。
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__ //Observer类实例
/**
* 对于数组 新增元素的 监测
*/
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args // 对于push和unshift 传入的参数就是要 新增的元素
break
case 'splice':
inserted = args.slice(2) // 对于splice 下标为2的参数就是新增的元素
break
}
if (inserted) ob.observeArray(inserted) // observeArray 将新增的元素转化为响应式
ob.dep.notify()
return result
})
})
8.总结
因为数组的变化侦测是通过拦截器拦截数组的操作方法实现的,所以我们平常使用数组的下标来操作数据就不会触发vue的响应式更新。
解决方法:不用下标操作数组,改为用数组方法。或者使用Vue.$set