- 7.12 数组检测
- 7.12.1 数组方法的重写
- 7.12.2 依赖收集
- 7.12.3 派发更新
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.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作为依赖保存,dependArray保证了如果数组元素是数组或者对象,需要递归去为内部的元素收集相关的依赖。
function dependArray (value) {for (var e = (void 0), i = 0, l = value.length; i < l; i++) {e = value[i];e && e.__ob__ && e.__ob__.dep.depend();if (Array.isArray(e)) {dependArray(e);}}}
我们可以通过截图看最终依赖收集的结果。
收集前

收集后

7.12.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;breakcase 'splice':inserted = args.slice(2);break}if (inserted) { ob.observeArray(inserted); }// notify changeob.dep.notify();return result});});
mutator是重写的数组方法,首先会调用原始的数组方法进行运算,这保证了与原始数组类型的方法一致性,args保存了数组方法调用传递的参数。之后取出数组的__ob__也就是之前保存的Observer实例,调用ob.dep.notify();进行依赖的派发更新,前面知道了。Observer实例的dep是Dep的实例,他收集了需要监听的watcher依赖,而notify会对依赖进行重新计算并更新。具体看Dep.prototype.notify = function notify () {}函数的分析,这里也不重复赘述。
回到代码中,inserted变量用来标志数组是否是增加了元素,如果增加的元素不是原始类型,而是数组对象类型,则需要触发observeArray方法,对每个元素进行依赖收集。
Observer.prototype.observeArray = function observeArray (items) {for (var i = 0, l = items.length; i < l; i++) {observe(items[i]);}};
总的来说。数组的改变不会触发setter进行依赖更新,所以Vue创建了一个新的数组类,重写了数组的方法,将数组方法指向了新的数组类。同时在访问到数组时依旧触发getter进行依赖收集,在更改数组时,触发数组新方法运算,并进行依赖的派发。
现在我们回过头看看Vue的官方文档对于数组检测时的注意事项:
Vue不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue- 当你修改数组的长度时,例如:
vm.items.length = newLength
显然有了上述的分析我们很容易理解数组检测带来的弊端,即使Vue重写了数组的方法,以便在设置数组时进行拦截处理,但是不管是通过索引还是直接修改长度,都是无法触发依赖更新的。
