7.8.数组检测
在深入剖析Vue源码 - 数据代理,关联子父组件这一节中,已经详细介绍了vue
数据代理的技术是利用了Object.defineProperty
,有了Object.defineProperty
方法,我们可以方便的利用存取描述符中的getter/setter
来进行数据的监听,在get,set
钩子中分别做不同的操作,达到数据拦截的目的。然而Object.defineProperty
的get,set
方法只能检测到对象属性的变化,对于数组的变化(例如插入删除数组元素等操作),Object.defineProperty
却无法检测,这也是利用Object.defineProperty
进行数据监控的缺陷,虽然es6
中的proxy
可以完美解决这一问题,但毕竟有兼容性问题,所以我们还需要研究Vue
中如何对数组进行监听检测。
7.8.1 数组方法的重写
数组的改变不能再通过数据的setter
方法去监听数组的变化,所以只能通过调用数组方法后对数据进行额外的处理。Vue
为所有数组操作的方法重新改写了定义。
var arrayProto = Array.prototype;
// 新建一个继承于Array的对象
var arrayMethods = Object.create(arrayProto);
// 数组拥有的方法
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
arrayMethods
是基于原始Array
类为原型继承的一个对象类,因此也拥有数组的所有方法,接下来对新数组类的方法进行改写。
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
});
}
当调用新对象的数组方法时,会调用mutator
方法,具体执行内容,我们放到数组的派发更新中介绍。
新建了一个定制化的数组类arrayMethods
后,如何在调用数组方法时指向这个新的类,这是下一步的重点。
回到数据初始化,也就是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
的判断执行protoAugment, copyAugment
过程,hasProto
用来判断当前环境下是否支持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.8.2 依赖收集
由于数据初始化阶段会利用Object.definePrototype
进行数据访问的改写,数组的访问同样适用,因此当访问到的数据是数组时,会被getter
拦截处理,这里针对数组进行特殊处理。
function defineReactive() {
···
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.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set() {}
}
childOb
是标志属性值是否为基础类型的标志,observe
遇到基本类型数据直接返回,不做任何处理,遇到对象和数组则会递归实例化Observer
,最终返回Observer
实例。而实例化Observer
又回到之前的老流程:添加ob
属性,如果遇到数组则进行原型重指向,遇到对象则定义getter,setter
,这一过程前面分析过,就不再阐述。
在访问到数组时,由于childOb
的存在,会执行childOb.dep.depend();
进行依赖收集,该Observer
实例的dep
属性会收集当前的watcher
作为依赖保存,这就是依赖收集的过程。
我们可以通过截图看最终依赖收集的结果。
收集前
收集后
7.8.3 派发更新
当调用数组的方法改变数组元素时,数据的setter
方法是无法拦截的,所以我们唯一可以拦截的过程就是调用数组方法的时候,前面介绍过,数组方法的调用会代理到新类arrayMethods
的方法中,而arrayMethods
的数组方法是进行重写过的。具体我们看他的定义。
methodsToPatch.forEach(function (method) {
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
// 执行原数组方法
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
mutator
是重写的数组方法,首先会调用原始的数组方法进行运算,这保证了与原始数组类型的方法一致性,args
保存了数组方法调用传递的参数。之后取出数组的ob
也就是之前保存的Observer
实例,调用ob.dep.notify();
进行依赖的派发更新,前面知道了。Observer
实例的dep
是Dep
的实例,他收集了需要监听的watcher
依赖,而notify
会对依赖进行重新计算并更新。具体看Dep.prototype.notify = function notify () {}
函数的分析,这里也不重复赘述。
回到代码中,inserted
变量用来标志数组是否是增加了元素,如果增加的元素不是原始类型,而是数组对象类型,则需要触发observeArray
方法,对每个元素进行依赖收集。
总的来说。数组的改变不会触发setter
进行依赖更新,所以Vue
创建了一个新的数组类,重写了数组的方法,将数组方法指向了新的数组类。同时在返回到数组时依旧触发getter
进行依赖收集,在更改数组时,触发数组新方法运算,并进行依赖的派发。
现在我们回过头看看Vue的官方文档对于数组检测时的注意事项:
Vue 不能检测以下数组的变动当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue当你修改数组的长度时,例如:vm.items.length = newLength
有了上诉的分析,数组的这些设置方式确实不会触发派发更新的过程。