需要您明白大致的Vue响应式原理相关知识(Observer
、Dep
、Watcher
)
开始
官方文档
Vue 不能检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
导致上边的原因是因为对象通过Object.defineproperty
代理,但是数组无法通过这种方式代理,Vue采用的方法是代理数组的方法(push
,pop
,shift
,unshift
,splice
,sort
,reverse
)来达到响应式的目的
代码
下面将整体流程按函数区分来说下
和对象代理部分的区别
数组和对象处理的区别是在class Observer{}
中开始区分的,对象的话是调用了walk
方法进行处理,数组的话是首先执行protoAugment
/copyAugment
方法来“劫持”数组七种方法,然后调用observeArray(value)
将数组中的属性循环执行observe(item)
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
// 这里的dep:
// 创建一个多数用于数组使用的dep
// 详情可以看作者的其他文章
this.dep = new Dep()
this.vmCount = 0
// 为value添加"__ob__"属性,值为this
def(value, '__ob__', this)
// -------------这里是处理数组的逻辑-------------
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 调用observeArray去代理数组内部的属性
this.observeArray(value)
} else {
// 处理对象操作,这里不管他
this.walk(value)
}
}
// 处理数组中子集的方法,因为数组中除了属性,也可能会有数组|对象等数据,在这里对子属性重新执行observe函数进行校验与代理
observeArray (items: Array<any>) {
// 就是递归了一下,循环执行observe
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
劫持数组的方法
劫持数组的方法实际上都已经生成完毕,包装在arrayMethods
这个对象中,Vue调用了两个不同的函数(protoAugment
、copyAugment
)去使得数组调用arrayMethods
中的那些方法,实际执行的内容是相似的,只是做了兼容性的处理,__proto__
是一个非标准属性,else
中的copyAugment
函数就是兼容了不支持__proto__
的浏览器
const hasProto = '__proto__' in {} // 判断__proto__能不能用
......
if (hasProto) {
protoAugment(value, arrayMethods) // 存在__proto__这个非标准属性
} else {
copyAugment(value, arrayMethods, arrayKeys) // 不存在时做的兼容处理
}
(__proto__
早已被大多数浏览器兼容)
劫持数组的方法
我们首先说下arrayMethods对象
在引入Vue后,arrayMethods对象中的方法就已经创建完成了
- 首先以
Array.prototype
为原型创建arrayMethods
空对象,这样做的目的是就算我们调用Vue
没有代理过的数组方法,也可以通过原型链找到原生的方法 - 创建策略名称数组
methodsToPatch
,里边是Vue
要代理的内容 - 循环
methodsToPatch
数组,在arrayMethods
中创建数组的新方法的方法
首先执行原生的方法
判断执行的方法会不会插入新的数据,如果插入的话,执行class Observer类中的observeArray()方法
将新的数据进行响应式代理
调用ob.dep.notify()
,通知组件更新视图
返回原生方法执行后的结果(返回返回值)
// 将原生的数组方法取到存起来
const arrayProto = Array.prototype
// 用原生的数组方法当做原型,创建arrayMethods空对象
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 循环methodsToPatch以在arrayMethods里创建对应methodsToPatch数组中的事件
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method] // 缓存原始的方法
// def: 用Object.defineProperty将该函数定义为arrayMethods[method]属性
// 也就是说,def中的参数function mutator 就是我们要创建的7个新方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) // 执行原生的方法
const ob = this.__ob__ // 获取当前属性的Observer实例
// 查看执行的方法中是不是有插入的新数据,如果有,把插入的新数据以数组的格式存到inserted中
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 对插入的新数据进行响应式绑定(observeArray方法在上边有写)
if (inserted) ob.observeArray(inserted)
// 最后调用notify,通知watcher更新视图
ob.dep.notify()
return result // 返回原生方法执行后的结果
})
})
用新方法替换老方法
protoAugment
有__proto__
的时候,没啥可说的,直接替换__proto__
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
copyAugment
没有__proto__
的时候,将上边创建的方法对象(arrayMethods
==== src
)便利,用Object.defineProperty放到target(数组)上,当数组.push()
或.其他方法时,首先调用的就是我们改造后的方法了
// 稍微改造了一下,使其更易懂
const arrayKeys = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
function copyAugment (target: Object, src: Object) {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(target, key, src[key])
}
}