Vue2剝絲抽繭-響應(yīng)式系統(tǒng)之?dāng)?shù)組
場(chǎng)景
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
list: ["hello"],
};
observe(data);
const updateComponent = () => {
for (const item of data.list) {
console.log(item);
}
};
new Watcher(updateComponent);
data.list = ["hello", "liang"];
先可以一分鐘思考下會(huì)輸出什么。
... ...
雖然 list 的值是數(shù)組,但我們是對(duì) data.list 進(jìn)行整體賦值,所以依舊會(huì)觸發(fā) data.list 的 set ,觸發(fā) Watcher 進(jìn)行重新執(zhí)行,輸出如下:
場(chǎng)景 2
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
list: ["hello"],
};
observe(data);
const updateComponent = () => {
for (const item of data.list) {
console.log(item);
}
};
new Watcher(updateComponent);
data.list.push("liang");
先可以一分鐘思考下會(huì)輸出什么。
... ...
這次是調(diào)用 push 方法,但我們對(duì) push 方法什么都沒(méi)做,因此就不會(huì)觸發(fā) Watcher 了。
方案
為了讓 push 還有數(shù)組的其他方法也生效,我們需要去重寫它們,通過(guò) 代理模式,我們可以將數(shù)組的原方法先保存起來(lái),然后執(zhí)行,并且加上自己額外的操作。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
/*
export function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}
*/
import { def } from "./util";
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
/*****************這里相當(dāng)于調(diào)用了對(duì)象 set 需要通知 watcher ************************/
// 待補(bǔ)充
/**************************************************************************** */
return result;
});
});
當(dāng)調(diào)用了數(shù)組的 push 或者其他方法,就相當(dāng)于我們之前重寫屬性的 set ,上邊待補(bǔ)充的地方需要做的就是通知 dep 中的 Watcher 。
export function defineReactive(obj, key, val, shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key);
// 讀取用戶可能自己定義了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 沒(méi)有傳進(jìn)來(lái)話進(jìn)行手動(dòng)賦值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
const dep = new Dep(); // 持有一個(gè) Dep 對(duì)象,用來(lái)保存所有依賴于該變量的 Watcher
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
dep.notify();
},
});
}
如上邊的代碼,之前的 dep 是通過(guò)閉包,每一個(gè)屬性都有一個(gè)各自的 dep ,負(fù)責(zé)收集 Watcher 和通知 Watcher 。
那么對(duì)于數(shù)組的話,我們的 dep 放到哪里比較簡(jiǎn)單呢?
回憶一下現(xiàn)在的結(jié)構(gòu)。
const data = {
list: ["hello"],
};
observe(data);
const updateComponent = () => {
for (const item of data.list) {
console.log(item);
}
};
new Watcher(updateComponent);
上邊的代碼執(zhí)行過(guò)后會(huì)是下圖的結(jié)構(gòu)。
list 屬性在閉包中擁有了 Dep 屬性,通過(guò) new Watcher ,收集到了包含 updateCompnent 函數(shù)的 Watcher。
同時(shí)因?yàn)?list 的 value ["hello"] 是數(shù)組,也就是對(duì)象,通過(guò)上篇 響應(yīng)式系統(tǒng)之深度響應(yīng) 我們知道,它也會(huì)去調(diào)用 Observer 函數(shù)。
那么,我是不是在 Observer 中也加一個(gè) Dep 就可以了。
這樣當(dāng)我們調(diào)用數(shù)組方法去修改 ['hello'] 的值的時(shí)候,去通知 Observer 中的 Dep 就可以了。
收集依賴代碼實(shí)現(xiàn)
按照上邊的思路,完善一下 Observer 類。
export class Observer {
constructor(value) {
/******新增 *************************/
this.dep = new Dep();
/************************************/
this.walk(value);
}
/**
* 遍歷對(duì)象所有的屬性,調(diào)用 defineReactive
* 攔截對(duì)象屬性的 get 和 set 方法
*/
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
然后在 get 中,當(dāng)前 Oberver 中的 dep 也去收集依賴。
export function defineReactive(obj, key, val, shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key);
// 讀取用戶可能自己定義了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 沒(méi)有傳進(jìn)來(lái)話進(jìn)行手動(dòng)賦值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
const dep = new Dep(); // 持有一個(gè) Dep 對(duì)象,用來(lái)保存所有依賴于該變量的 Watcher
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
/******新增 *************************/
if (childOb) {
// 當(dāng)前 value 是數(shù)組,去收集依賴
if (Array.isArray(value)) {
childOb.dep.depend();
}
}
/************************************/
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
dep.notify();
},
});
}
通知依賴代碼實(shí)現(xiàn)
我們已經(jīng)重寫了 array 方法,但直接覆蓋全局的 arrray 方法肯定是不好的,我們可以在 Observer 類中去操作,如果當(dāng)前 value 是數(shù)組,就去攔截它的 array 方法。
這里就回到 js 的原型鏈上了,我們可以通過(guò)瀏覽器自帶的 __proto__ ,將當(dāng)前對(duì)象的原型指向我們重寫過(guò)的方法即可。
考慮兼容性的問(wèn)題,如果 __proto__ 不存在,我們直接將重寫過(guò)的方法復(fù)制給當(dāng)前對(duì)象即可。
import { arrayMethods } from './array' // 上邊重寫的所有數(shù)組方法
/* export const hasProto = "__proto__" in {}; */
export class Observer {
constructor(value) {
this.dep = new Dep();
/******新增 *************************/
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
/************************************/
} else {
this.walk(value);
}
}
/**
* 遍歷對(duì)象所有的屬性,調(diào)用 defineReactive
* 攔截對(duì)象屬性的 get 和 set 方法
*/
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment(target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
還需要考慮一點(diǎn),數(shù)組方法中我們只能拿到 value 值,那么怎么拿到 value 對(duì)應(yīng)的 Observer 呢。
我們只需要在 Observe 類中,增加一個(gè)屬性來(lái)指向自身即可。
export class Observer {
constructor(value) {
this.dep = new Dep();
/******新增 *************************/
def(value, '__ob__', this)
/************************************/
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
} else {
this.walk(value);
}
}
...
}
回到最開(kāi)始重寫的 array 方法中,只需要從 __ob__ 中拿到 Dep 去通知 Watcher 即可。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from "./util";
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
/*****************這里相當(dāng)于調(diào)用了對(duì)象 set 需要通知 watcher ************************/
const ob = this.__ob__;
// notify change
ob.dep.notify();
/**************************************************************************** */
return result;
});
});
測(cè)試
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
list: ["hello"],
};
observe(data);
const updateComponent = () => {
for (const item of data.list) {
console.log(item);
}
};
new Watcher(updateComponent);
data.list.push("liang");
這樣當(dāng)調(diào)用 push 方法的時(shí)候,就會(huì)觸發(fā)相應(yīng)的 Watcher 來(lái)執(zhí)行 updateComponent 函數(shù)了。
當(dāng)前的依賴就變成了下邊的樣子:
總結(jié)
對(duì)于數(shù)組的響應(yīng)式我們解決了三個(gè)問(wèn)題,依賴放在哪里、收集依賴和通知依賴。
我們來(lái)和普通對(duì)象屬性進(jìn)行一下對(duì)比。