Vue:Array变化侦测
1. 数组追踪变化
与Object不同,数组无法通过getter和setter方式来追踪变化,因此,我们需要自定义一个拦截器来追踪变化。
2. 拦截器的准备
拦截器其实就是一个与Array.prototype一样的Object,里面所包含的属性一样,但是改变数组的方法是我们修改过的。
首先,我们先对Array.prototype来个定制。
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
let methods = ['push','pop','shift','unshift','splice','sort','reverse'];
methods.forEach(function(method){
// 获取原始方法
const original = arrayMethods[method];
Object.defineProperty(arrayMethods,method,{
value: function mutator(...args){
return original.apply(this,args);
},
enumerable: true,
writable: true,
configurable: true
})
})
- 上面代码中,我们对Array.prototype进行了拷贝,创建出新的对象arrayMethods,其中的mutator就是我们追踪变化的关键,未来将对其进行扩展。
3. 使用拦截器覆盖Array原型
要将一个数据转化成响应式的数据,需要通过Observer,在Observer里使用拦截器覆盖那些即将被转换成响应式的Array类型数据:
const hasProto = '__proto__' in {
}; // 检测是否可用proto
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value){
// 根据是否支持ES6来对原型对象进行覆盖
const arguments = hasProto? 'protoArguments' : 'copyArguments';
arguments(value,arrayMethods,arrayKeys);
}else{
this.walk(value);
})
}
......
}
function protoArguments(target,src,keys){
target.__proto__ = src;
}
// 递归复制属性
function copyArguments(target,src,keys){
for(let i=0;i<keys.length;i++){
const key = keys[i];
def(target,key,src[key]); // 将arrayMethods的方法添加到target中
}
}
4. 收集依赖
我们先来回顾一下收集依赖的类Dep:
class Dep{
constructor(){
this.subs = [];
}
addSub(sub){
this.subs.push(sub);
}
removeSub(sub){
let index = this.subs.indexOf(sub);
if(index > -1){
this.subs = this.subs.splice(index,1);
}
}
depend(){
if(window.target){
this.addSub(window.target);
}
}
notify(){
let subs = this.subs.splice();
for(let i=0;i<subs.length;i++){
subs[i].update();
}
}
}
与Object不一样的是,Array的依赖保存在Observer中,原因是这样做既能在getter中访问到Observer实例,又能在拦截器中访问到Observer实例。
class Observer{
constructor(value){
this.value = value;
this.dep = new Dep();
if(Array.isArray(value)){
// 这里不再赘述,直接用ES6语法
value.__proto__ = arrayMethods;
}else{
this.walk(value);
}
}
......
}
Array的收集依赖也是在defineReactive中收集的,当我们把依赖保存到Observer后,我们可以在getter中对其进行收集:
function defineReactive(data,key,val){
let childOb = observe(val); //为val创建Observer实例
let dep = new Dep();
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;
}
})
}
/*
为val创建一个Observer实例,创建成功返回新的Observer实例,若val已存在一个,则返回val
*/
function observe(val,asRootData){
if(typeof val !== 'object'){
return ;
}
let ob = null;
if(hasOwn(val,'__ob__') && val.__ob__.instanceof Observer){
ob = val.__ob__;
}else{
ob = new Observer(val);
}
return ob;
}
5. 在拦截器中获取Observer实例
由于Array拦截器是对原型的封装,因此可以在拦截器中访问到当前正在操作数组的this:
function def(obj,key,val,enumerable){
Object.defineProperty(obj,key,{
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
class Observer{
constructor(value){
this.value = value;
this.dep = new Dep();
def(value,'__ob__',this); // 在value上新增一个不可枚举的属性__ob__,即当前Observer实例
value.__proto__ = arrayMethods;
}
.......
}
通过上面的方式,我们就可以通过__ob__
来拿到Observer实例了,进而拿到dep实例。
__ob__
的作用还可以用来记录当前value是否被Observer转换成响应式数据。如果拥有ob属性,则说明他们是响应式的;如果没有,则通过new Observer将其转换成响应式的。
由于拦截器是原型方法,所以可以通过this.__ob__
来访问Observer实例。
let methods = ['push','pop','shift','unshift','splice','sort','reverse'];
methods.forEach(function(method){
// 获取原始方法
const original = arrayMethods[method];
Object.defineProperty(arrayMethods,method,{
value: function mutator(...args){
const ob = this.__ob__;
return original.apply(this,args);
},
enumerable: true,
writable: true,
configurable: true
})
})
6. 向数组依赖发送通知
想要发送通知,必须得先获取依赖,然后直接调用依赖的发送通知方法:
let methods = ['push','pop','shift','unshift','splice','sort','reverse'];
methods.forEach(function(method){
// 获取原始方法
const original = arrayMethods[method];
def(arrayMethods,method,function mutator(...args){
const result = original.apply(this,args);
const ob = this.__ob__;
ob.dep.notify(); // 发送通知
return result;
})
})
7. 侦测数组中元素的变化
在这之前我们侦测的变化,指的是数组自身的变化,包括新增或删除一个元素;那么,要如何侦测数组中每一项元素的改变呢?
class Observer{
constructor(value){
this.value = value;
def(value,'__ob__',this);
if(Array.isArray(value)){
this.observeArray(value);
}else{
this.walk(value);
}
}
// 侦测数组内的每一项数据
observeArray(items){
for(let i=0;i<items.length;i++){
observe(items[i]);
}
}
......
}
上面的代码中,我们对数组中每一项都执行了一遍new Observer,即全部元素都转化成响应式的。
8. 侦测新增元素
要侦测新增元素,首先得获取新增元素,我们可以使用Observer来实现:
let methods = ['push','pop','shift','unshift','splice','sort','reverse'];
methods.forEach(function(method){
// 获取原始方法
const original = arrayMethods[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.observeArray(inserted);
ob.dep.notify(); // 发送通知
return result;
})
})
9. 总结
-
Array与Object的追踪方式不同。Array是通过方法来改变内容的,因此我们需要创建拦截器去覆盖数组原型的方式来追踪变化;
-
为了不污染全局Array.prototype,我们在Observer只针对需要侦测变化的数组,使用ES6的
__proto__
属性来覆盖其原型方法;对于不支持ES6语法的浏览器,我们直接循环拦截器,将所有方法直接设置到数组身上来拦截数组原型的方法; -
Array的依赖保存在Observer实例上;
-
在Observer中,我们对数组每一项元素都做了侦测并印上了标记
__ob__
,并把this保存起来,目的是:一是标记数据已经被侦测化;二是可以很方便拿到__ob__
,从而拿到保存在Observer的依赖。方便后续通知。 -
对于新增数据,我们对当前操作数组的方法进行判断,如果为’push’,‘unshift’,‘splice’,我们就从参数中将数据提取出来,然后使用observeArray方法对其进行数据侦测。