如何追蹤變化
為什么對(duì)于Array的偵測(cè)方式和Object的不同?如下一句push操作,調(diào)用的是數(shù)組原型上的方法改變數(shù)組,不會(huì)觸發(fā)getter/setter。
this.list.push(1);
在ES6之前,JavaScript并沒有提供元編程的能力,足以攔截原型方法。Vue的做法是寫自定義方法覆蓋原型方法。

用一個(gè)攔截器覆蓋Array.prototype,每當(dāng)我們調(diào)用原型方法操作數(shù)組時(shí),調(diào)用的都是自定義方法,就可以跟蹤到變化了。
攔截器
攔截器和Array.prototype一樣也是一個(gè)對(duì)象,包含的屬性也一樣,只是一些能改變數(shù)組的方法是處理過的。
整理一下,發(fā)現(xiàn)數(shù)組原型可以改變數(shù)組自身內(nèi)容的方法有七個(gè):push、pop、shift、unshift、splice、sorte和reverse。
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sorte',
'reverse'
].forEach(function(method){
// 緩存原始方法
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args){
return original.apply(this, args);
},
enumerable: false,
writeable: ture,
configurable: true
})
})
這樣我們就可以在mutator函數(shù)中做一些事情了,比如發(fā)送變化的通知。
使用攔截器覆蓋Array原型
export class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value)){
value.__proto__ = arrayMethods;
} else {
this.walk(value);
}
}
}
__proto__其實(shí)是Object.getPrototypeOf和Object.setPrototypeOf的早期實(shí)現(xiàn),只是ES6的瀏覽器支持度不理想。

將攔截器方法掛載到數(shù)組屬性上
并不是所有瀏覽器都支持通過__proto__訪問原型,所以還要處理不能使用這個(gè)非標(biāo)準(zhǔn)屬性的情況。
Vue的做法非常粗暴,直接將arrayMethods身上的方法設(shè)置到被偵測(cè)數(shù)組上。
const hasProto = '__proto__' in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value)){
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value);
}
}
}
function protoAugment(target, src, keys){
target.__proto__ = src;
}
function copyAugment(target, src, keys){
for(let i = 0, l = keys.length;i < l;i++){
const key = keys[i];
def(target, key, src[key]);
}
}
如何收集依賴
我們創(chuàng)建攔截器實(shí)際上是為了獲得一種能力,一種感知數(shù)組內(nèi)容發(fā)生變化的能力?,F(xiàn)在具備了這個(gè)能力,要通知誰(shuí)呢?根據(jù)前面對(duì)Object的處理,通知Dep中的依賴(Watcher)。
怎么收集依賴呢?還用getter。
function defineReactive(data, key, val){
if(typeof val = 'object'){
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
dep.depend();
// 這里收集依賴
return val;
},
set: function(newVal){
if(val === newVal){
return;
}
dep.notify();
val = newVal;
}
})
}
新增了一段注釋,也就是說(shuō)Array在getter中收集依賴,在攔截器觸發(fā)依賴。
依賴收集在哪
export class Observer{
constructor(value){
this.value = value;
this.dep = new Dep(); // 新增dep
if(Array.isArray(value)){
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value);
}
}
}
Vue將依賴列表存在了Observer,為什么是這里?
前面說(shuō)Array在getter中收集依賴,在攔截器觸發(fā)依賴,所以依賴的位置很關(guān)鍵,保證getter要訪問的到,攔截器也訪問的到。
收集依賴
Dep實(shí)例保存在Observer的屬性上后,我們開始收集依賴。
function defineReactive(data, key, val){
let childOb = observe(val); // 修改
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;
}
})
}
export function observe(value, asRootData){
if(!isObject(value)){
return;
}
let ob;
if(hasOwn(value, '__ob__')&&value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = observe(val);
}
return ob;
}
增加一個(gè)childOb 的意義到底是啥?在于搭建了從getter把依賴收集到Observer的dep中的橋梁。
在攔截器中獲取Observer
因?yàn)閿r截器是對(duì)數(shù)組原型的封裝,所以攔截器可以訪問到this(正在被操作的數(shù)組)。而dep在Observer中,所以需要在this上讀到Observer實(shí)例。
// 工具函數(shù)
function def(obj, key, val, enumerable){
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writeable: true,
configurable: true
})
}
export class Observer{
constructor(value){
this.value = value;
def(value, '__ob__', this); // 新增
if(Array.isArray(value)){
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value);
}
}
}
現(xiàn)在Observer實(shí)例已經(jīng)存入數(shù)組中__ob__屬性,下一步就是在攔截器中獲取。
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sorte',
'reverse'
].forEach(function(method){
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args){
const ob = this.__ob__; // 新增
return original.apply(this, args);
},
enumerable: false,
writeable: ture,
configurable: true
})
})
向數(shù)組的依賴發(fā)通知
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sorte',
'reverse'
].forEach(function(method){
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args){
const ob = this.__ob__;
ob.dep.notify(); // 向依賴發(fā)通知
return original.apply(this, args);
},
enumerable: false,
writeable: ture,
configurable: true
})
})
既然能獲取到Observer實(shí)例和里面的依賴列表了,就直接調(diào)用notify。
剩下的內(nèi)容就是獲取數(shù)組元素變化,以及Vue的處理方式的弊端,另開一篇寫吧。