上一节,我们深入分析了以
data,computed
为数据创建响应式系统的过程,并对其中依赖收集和派发更新的过程进行了详细的分析。然而在使用和分析过程中依然存在或多或少的问题,这一节我们将针对这些问题展开分析,最后我们也会分析一下watch
的响应式过程。这篇文章将作为响应式系统分析的完结篇。
7.12 数组检测
在之前介绍数据代理章节,我们已经详细介绍过Vue
数据代理的技术是利用了Object.defineProperty
,Object.defineProperty
让我们可以方便的利用存取描述符中的getter/setter
来进行数据的监听,在get,set
钩子中分别做不同的操作,达到数据拦截的目的。然而Object.defineProperty
的get,set
方法只能检测到对象属性的变化,对于数组的变化(例如插入删除数组元素等操作),Object.defineProperty
却无法达到目的,这也是利用Object.defineProperty
进行数据监控的缺陷,虽然es6
中的proxy
可以完美解决这一问题,但毕竟有兼容性问题,所以我们还需要研究Vue
在Object.defineProperty
的基础上如何对数组进行监听检测。
7.12.1 数组方法的重写
既然数组已经不能再通过数据的getter,setter
方法去监听变化了,Vue
的做法是对数组方法进行重写,在保留原数组功能的前提下,对数组进行额外的操作处理。也就是重新定义了数组方法。
var arrayProto = Array.prototype;
// 新建一个继承于Array的对象
var arrayMethods = Object.create(arrayProto);
// 数组拥有的方法
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
arrayMethods
是基于原始Array
类为原型继承的一个对象类,由于原型链的继承,arrayMethod
拥有数组的所有方法,接下来对这个新的数组类的方法进行改写。
methodsToPatch.forEach(function (method) {
// 缓冲原始数组的方法
var original = arrayProto[method];
// 利用Object.defineProperty对方法的执行进行改写
def(arrayMethods, method, function mutator () {
});
});
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
这里对数组方法设置了代理,当执行arrayMethods
的数组方法时,会代理执行mutator
函数,这个函数的具体实现,我们放到数组的派发更新中介绍。
仅仅创建一个新的数组方法合集是不够的,我们在访问数组时,如何不调用原生的数组方法,而是将过程指向这个新的类,这是下一步的重点。
回到数据初始化过程,也就是执行initData
阶段,上一篇内容花了大篇幅介绍过数据初始化会为data
数据创建一个Observer
类,当时我们只讲述了Observer
类会为每个非数组的属性进行数据拦截,重新定义getter,setter
方法,除此之外对于数组类型的数据,我们有意跳过分析了。这里,我们重点看看对于数组拦截的处理。
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。
def(value, '__ob__', this);
// 数组处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
// 对象处理
this.walk(value);
}
}
数组处理的分支分为两个,hasProto
的判断条件,hasProto
用来判断当前环境下是否支持__proto__
属性。而数组的处理会根据是否支持这一属性来决定执行protoAugment, copyAugment
过程,
// __proto__属性的判断
var hasProto = '__proto__' in {
};
当支持__proto__
时,执行protoAugment
会将当前数组的原型指向新的数组类arrayMethods
,如果不支持__proto__
,则通过代理设置,在访问数组方法时代理访问新数组类中的数组方法。
//直接通过原型指向的方式
function protoAugment (target, src) {
target.__proto__ = src;
}
// 通过数据代理的方式
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
有了这两步的处理,接下来我们在实例内部调用push, unshift
等数组的方法时,会执行arrayMethods
类的方法。这也是数组进行依赖收集和派发更新的前提。
7.12.2 依赖收集
由于数据初始化阶段会利用Object.definePrototype
进行数据访问的改写,数组的访问同样会被getter
所拦截。由于是数组,拦截过程会做特殊处理,后面我们再看看dependArray
的原理。
function defineReactive###1() {
···
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.