看文章之前需要具备Vue2关于Object的变化监测的相关认识,因为代码部分有些省略掉的内容在之前讨论Object变化文章中的内容里面这里去看Ojbect变化监测
另外还需要点原型链的知识这里去看原型链
一、拦截器
情形之一:Vu2里面对数组直接使用下标进行修改,例如 arr[0] = 1,这样确实真的让arr[0]变为1,但是屏幕上使用到arr[0]并没有变为1。
同时也了解到Vue2中使用以下的7个数组方法[‘push’,‘pop’,‘shift’,‘unshift’,‘splice’,‘sort’,‘reverse’](这7个方法均改变原来数组)在改变数组本身的同时也会让用到数组的地方知道这个数组发生了改变。
实质上就是Vue2把这个7个方法重新包装了下,在调用函数之前搞些其他东西。
const arrayProto = Array.prototype;
arrayMethods = Object.create(arrayProto);//将Array.prototype复制一份,避免污染全局
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(NameOfFn){
const originalFn = arrayProto[NameOfFn];//拿出对应的函数
//在Array.prototype复制对象中重写方法
Object.defineProperty(arrayMethods,NameOfFn,{
value:function mutator(...args)
{
return originalFn.apply(this,args);
},
enumerable:false,
writable:true,
configurable:true
})
})
由于这7个方法均在Array.prototype里面,那么拿到Array.prototype即可同时为了避免污染Array.prototype,对Array.prototype进行复制一份。之后将数组实例的原型对象指向复刻的Array.prototype即可。例如 arr.__proto__ = arrayMethods,之后arr使用7个方法的时候便按照原型链找,找到 arrayMethods里面重新写好的7个方法。在这里称arrayMethods为拦截器。
引用自《深入浅出vue.js》
要是不支持__proto__,则直接将包装方法塞到数组身上去
/*Observer的用途是将数据里面的属性均变为响应式,
那么数组想变为为响应式数据就需要用到拦截器,
因此需要在Observer里面设置拦截器
*/
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);//把所有属性名拿出来
class Observer{
constructor(value)
{
this.value = value;
if(Array.isArray(value))
{
// 数组实例的原型对象改为拦截器
const augment = hasProto?protoAugment:copyAugment
augment(value,arrayMethods,arrayKeys)
}
else{
this.walk(value);
}
}
walk(obj)
{
const keys = Object.keys(obj);
for(let i = 0; i< keys.length;i++)
{
//将每个属性变为响应式
defineReactve(obj,keys[i],obj[keys[i]]);
}
}
}
//有__proto__
function protoAugment(target,src)
{
target.__proto__= src
}
//没有__proto__,便直接把arrayMethods所有属性写在当前数组实例身上
function copyAugment(target,src,keys)
{
for(let i = 0,l = keys.lenght;i < l; i++)
{
const key = keys[i];
def(target,key,src[key])//将函数绑定到target身上
}
}
function def(obj,key,val,enumerable)
{
Object.defineProperty(obj,key,{
value:val,
enumerable:!!enumerable,//双重!!不会改变boolean值性
writable:true,
configurable:true
})
}
二、依赖的收集、存储与触发
有了拦截器之后,相当于获得了通知依赖的能力(就是我们知道数据即将发生变动,将数据要变动这个消息告诉其他地方)。现在是还不知道要告诉谁,那想要知道告诉谁要提前收集好名单。
Object监测里面在getter里面收集依赖,同样地数组也是在getter里面收集依赖。因为读取数组的时候会访问属性名例如
a={
b:[1,2,3]
}
//访问数组b 需要a.b,此时便触发了getter知道是谁想访问数组了
跟监测Object不一样的是,Vue2在Observer里面存储依赖。存储依赖的地方必须可以在getter里面访问到(getter收集依赖)并且可以在拦截器中访问到(拦截器触发依赖),而Observer符合这一设定
//存储依赖
class Observer{
constructor(value)
{
this.value = value;
this.dep = new Dep();//改动
if(Array.isArray(value))
{
const augment = hasProto?protoAugment:copyAugment
augment(value,arrayMethods,arrayKeys)
}
else{
this.walk(value);
}
}
/*
walk函数
*/
}
/*
observe函数判断此对象是否已经有了Observer实例对象
并且Observer实例对象放在数据的__ob__属性上
*/function observe(value)
{
if(!isObject(value))
{ //数据并非对象
return ;
}
let ob;
//hasOwn
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer)
{ //若这个对象已经有__ob__且指向Observer实例时直接返回这个实例
ob = value.__ob__
}else
{
ob = new Observer(value)
}
return ob;
}
//新的defineReactive函数,融合了数组收集依赖,
//即使为普通对象也会在Observer里面额外收集依赖
function defineReactive(data,key,val)
{
let childOb = observe(val);//不论数组亦或是普通对象均要进去
let dep = new Dep()
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get()
{
dep.depend();
if(childOb)
{
childOb.dep.depend();//对象、数组收集依赖
}
return val
},
set(newVal)
{
if(val === newVal)
{
return
}
dep.notify();
val = newVal;
}
})
}
下面介绍拦截器里面如何访问Observer实例对象,将
class Observer{
constructor(value)
{
this.value = value ;
this.dep = new Dep()
def(value,"__ob__",this)//def的介绍在上面
/* 将observer实例绑定到响应式数据(不论数组或对象)__ob__上
那么当此数据为数组的时候我们可以通过数组__ob__这个属性访问到
observer实例对象,而observer实例含有dep数组
进一步地当这个对象是数组的原型对象Array.prototype时
数组实例通过隐式原型链__proto__来访问到其原型对象身上的__ob__
*/
if(Array.isArray(value))
{
const augment = hasProto?protoAugment:copyAugment;
//arrayMethods复制的原型对象,arrayKeys:复制的原型对象中的属性名
augment(value,arrayMethods,arrayKeys);
}
else
{
this.walk(value)
}
/*
....
*/
}
}
触发依赖
/*
注意下面[]前面记得加个;
否则前面没有分号的话便会出错的。
*/
;['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(NameOfFn){
const originalFn = arrayProto[NameOfFn];//拿出数组原型对象对应的函数
/*
对复制的Array.prototy对象的
['push','pop','shift','unshift','splice','sort','reverse']所有方法重写
调用重写的方法时会产生如下效果
1.拿出之前在复制的Array.prototy对象新建的属性__ob__(它指向observer实例)
2.通知observer实例中已经收集好的依赖
*/
def(arrayMethods,NameOfFn,function mutator(...args)
{
const result = originalFn.apply(this,args);//谁调用def this指向那个对象
const ob = this.__ob__;//相当于arrayMethods.__ob__ = observer实例
ob.dep.notify();
return result
})
})
三、监测变化、将新添加的数据变为响应式数据
监测变化
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,l = items.length; i <l ;i++)
{
observe(items[i]);
}
}
}
新的数据变为响应式
;['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(NameOfFn){
const originalFn = arrayProto[NameOfFn];//拿出数组原型对象对应的函数
def(arrayMethods,NameOfFn,function mutator(...args)
{
//args就是调用函数的时候传进来的参数,例如 arr.splice(1,2,3) args = [1,2,3]
const result = originalFn.apply(this,args);//谁调用def this指向那个对象
const ob = this.__ob__;
let inserted;
switch(NameOfFn)
{
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
/* splice函数==> arr.splice(1,2,3)从下标为1开始删除的个数为2个
删除完之后在被删除的位置整体只添加一个新数据3
例如 arr=[a,b,c,d,e] => [a,3,d,e]
因此这里的args.slice(2)是想要获取插入的新数据
splice(1,2,3) 的时候 args.slice(2) = 3
*/
inserted = args.slice(2);
break;
}
if(inserted)
{
//将新增的数据变为响应式数据
ob.observeArray(inserted);
}
ob.dep.notify();
return result
})
})
总结:
Vue2中的数组监测:在getter里面收集依赖,在Observer里面存储依赖,拦截器放在Observer里面同时由拦截器进行触发依赖。
从上面简单的讨论就可以知道Vue2是在拦截器里面通知依赖的,如果你绕过这个拦截器或者说不能让拦截器生效的话那自然做不到所见即所得。