上一章我们讲到,vue对于对象的变化侦测是通过Object.defineProperty方法(实则是通过getter/setter)来实现的。本章我们来讲解vue对于数组的变化侦测是如何实现的。
Array不同于Object,我们在使用的时候其实是通过Array原型上的方法来改变数组的内容的,因此侦测Object变化的那种方式就行不通了。
侦测Array变化的拦截器
拦截器其实就是一个和Array.prototype一样的Object,里面的书香和数组原型的一模一样,只不过其中一些方法是经过我们处理的。
Array原型中可以改变数组自身内容的方法有7个:
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
下面是我们创建的拦截器:
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
;['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
// 缓存原始方法
const original = arrayProto[method];
Object.defineProperty(arrayMethods,method,{
value: function mutator(...args){
return original.apply(this,args);
},
enumerable: false,
writable: true,
configurable: true
})
})
使用拦截器覆盖Array原型
现在有了拦截器之后,我们需要用它去覆盖数组的原型,但是我们希望它只是覆盖那些响应式数组的原型。
上一章我们说过,将一个数据转换成响应式的数据,需要通过Observer,因此,Observer做出如下改变:
export class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value)){
value.__proto__ = arrayMethods;
}else{
this.walk(value);
}
}
...
}
原理如下图所示:
将拦截器方法挂在到数组的属性上
这里有个问题就是非标准属性__proto__属性在浏览器中的支持问题:
vue的做法是如果浏览器不支持__proto__这个属性,就直接将arrayMethods身上的这些方法设置到被侦测的数组实例上。
import {arrayMethods} from './array'
const hasProto = '__proto__' in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value)){
const argument = hasProto? protoArgument: copyArgument;
argument(value,arrayMethods,arrayKeys);
}else{
this.walk(value);
}
}
...
}
funtion protoArgument(target,src,keys){
target.__proto__ = src;
}
function copyArgument(target,src,keys){
for(let i=0,len=keys.length;i<len;i++){
const key = keys[i];
def(target,key,src[key]);
}
}
如何收集依赖,依赖列表存在何处
数组其实也是在getter中收集依赖,在拦截器中触发依赖。
为什么呢?举个例子:
{
list: [1,2,3,4]
}
要想得到上面这段代码中的list数组,那么你一定需要通过list这个key来读取,所以要在Object中通过key来读取数据就一定会触发这个key对应的getter
vue把Array的依赖存放在Observer中:
原因就是数组的依赖存放的位置必须能让getter和拦截器都能访问到。
export class Observer{
constructor(val){
this.value = value;
this.dep = new Deep();
if(Array.isArray(value)){
const argument = hasProto? protoArgument: copyArgument;
argument(value,arrayMethods,arrayKeys);
}else{
this.walk(value);
}
}
...
}
收集Array的依赖的代码(getter中访问Observer实例)如下:
function defineReactive(data,key,val){
if(typeof val === 'object') new Observer(val);
let childOb = observe(val); // 得到一个Observer实例
let dep = new Deep();
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get: function(){
dep.depend();
if(childOb) childOb.dep.depend(); // 为数组收集依赖
return val;
},
set: function(newVal){
if(val === newVal) return;
dep.notify();
val = newVal;
}
})
}
export function observe(value,asRootData){
if(!isObject(value)) return;
let ob;
// 避免重复侦测value变化,value.__ob__是Observer的实例
if(hasOwn(value,'__proto__') && value.__ob__ instanceof Observer){
ob = value.__ob__;
}else{
ob = new Observer(value);
}
return ob;
}
上面代码中出现了__ob__,大家可能比较疑惑它是什么,接下来给大家解惑。
在拦截器中如何访问Observer实例
由于Array拦截器是对原型的封装,所以它是可以访问到this的,因此我们需要在this上读取Observer实例。
// 工具函数
function def(obj,key,val,enumerable){
Object.defineProperty(obj,key,{
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
export class Observer{
constructor(val){
this.value = value;
this.dep = new Deep();
def(value,'__ob__',this);
if(Array.isArray(value)){
const argument = hasProto? protoArgument: copyArgument;
argument(value,arrayMethods,arrayKeys);
}else{
this.walk(value);
}
}
...
}
因此,我们最初创建的数组拦截器就可以访问Observer实例并且触发依赖了:
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
;['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
// 缓存原始方法
const original = arrayProto[method];
Object.defineProperty(arrayMethods,method,{
value: function mutator(...args){
const ob = this.__ob__; // Observer实例
ob.dep.notify(); // 触发依赖
return original.apply(this,args);
},
enumerable: false,
writable: true,
configurable: true
})
})
侦测数组中元素的变化
现在Observer不光能处理Object类型的数据,还能将Array也转换成响应式的。
export class Observer{
constructor(val){
this.value = value;
def(value,'__ob__',this);
if(Array.isArray(value)){
this.ovserveArray(value);
}else{
this.walk(value);
}
}
...
}
observerArray(list){
for(let i=0,len=list.length;i<len;i++){
observe(list[i]);
}
}
侦测新增元素的变化
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
;['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
// 缓存原始方法
const original = arrayProto[method];
def(arrayMethods,method,function mutator(...args){
const result = original.apply(this,args);
const ob = this.__ob__;
let inserted;
switch(method){
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observerArray(inserted); // 新增元素转换成响应式数据
ob.dep.notify();
return result;
})
})
关于Array变化侦测存在的问题
- 修改数组中第一个元素的值时(this.list[0] = 2),无法侦测到数组的变化
- 清空数组的操作(this.list.length = 0),无法侦测到数组的变化