Vue2剝絲抽繭-響應式系統(tǒng)之嵌套
場景
在 Vue 開發(fā)中肯定存在組件嵌套組件的情況,類似于下邊的樣子。
<!-- parent-component -->
<div>
<my-component :text="inner"></my-component>
{{ text }}
<div>
<!-- my-component-->
<div>{{ text }}</div>
回到我們之前的響應式系統(tǒng),模擬一下上邊的情況:
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
inner: "內(nèi)部",
};
observe(data);
const updateMyComponent = () => {
console.log("子組件收到:", data.inner);
};
const updateParentComponent = () => {
new Watcher(updateMyComponent);
console.log("父組件收到:", data.text);
};
new Watcher(updateParentComponent);
data.text = "hello, liang";
可以先 1 分鐘考慮一下上邊輸出什么?
首先回憶一下 new Watcher 會做什么操作。
第一步是保存當前函數(shù),然后執(zhí)行當前函數(shù)前將全局的 Dep.target 賦值為當前 Watcher 對象。
接下來執(zhí)行 getter 函數(shù)的時候,如果讀取了相應的屬性就會觸發(fā) get ,從而將當前 Watcher 收集到該屬性的 Dep 中。
執(zhí)行過程
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
inner: "內(nèi)部",
};
observe(data);
const updateMyComponent = () => {
console.log("子組件收到:", data.inner);
};
const updateParentComponent = () => {
new Watcher(updateMyComponent);
console.log("父組件收到:", data.text);
};
new Watcher(updateParentComponent);
data.text = "hello, liang";
我們再一步一步理清一下:
- new Watcher(updateParentComponent);
將 Dep.target 賦值為保存了 updateParentComponent 函數(shù)的 Watcher 。
接下來執(zhí)行 updateParentComponent 函數(shù)。
- new Watcher(updateMyComponent);
將 Dep.target 賦值為保存了 updateMyComponent 函數(shù)的 Watcher 。
接下來執(zhí)行 updateMyComponent 函數(shù)。
const updateMyComponent = () => {
console.log("子組件收到:", data.inner);
};
// 讀取了 inner 變量。
// data.inner 的 Dep 收集當前 Watcher(保存了 `updateMyComponent` 函數(shù))
const updateParentComponent = () => {
new Watcher(updateMyComponent);
console.log("父組件收到:", data.text);
};
// 讀取了 text 變量。
// data.text 的 Dep 收集當前 Watcher (保存了 `updateMyComponent` 函數(shù))
- data.text = "hello, liang";
觸發(fā) text 的 set 函數(shù),執(zhí)行它依賴的 Watcher ,而此時是 updateMyComponent 函數(shù)。
所以上邊代碼最終輸出的結果是:
子組件收到: 內(nèi)部 // new Watcher(updateMyComponent); 時候輸出
父組件收到:hello, world // new Watcher(updateParentComponent); 時候輸出
子組件收到: 內(nèi)部 // data.text = "hello, liang"; 輸出
然而子組件并不依賴 data.text,依賴 data.text 的父組件反而沒有執(zhí)行。
修復
上邊的問題出在我們保存當前正在執(zhí)行 Watcher 時候使用的是單個變量 Dep.target = null; // 靜態(tài)變量,全局唯一。
回憶一下學習 C 語言或者匯編語言的時候對函數(shù)參數(shù)的處理:
function b(p) {
console.log(p);
}
function a(p) {
b("child");
console.log(p);
}
a("parent");
當函數(shù)發(fā)生嵌套調(diào)用的時候,執(zhí)行 a 函數(shù)的時候我們會先將參數(shù)壓入棧中,然后執(zhí)行 b 函數(shù),同樣將參數(shù)壓入棧中,b 函數(shù)執(zhí)行完畢就將參數(shù)出棧。此時回到 a 函數(shù)就能正確取到 p 參數(shù)的值了。
對應于 Watcher 的收集,我們同樣可以使用一個棧來保存,執(zhí)行函數(shù)前將 Watcher 壓入棧,執(zhí)行函數(shù)完畢后將 Watcher 彈出棧即可。其中,Dep.target 始終指向棧頂 Watcher ,代表當前正在執(zhí)行的函數(shù)。
回到 Dep 代碼中,我們提供一個壓棧和出棧的方法。
import { remove } from "./util";
let uid = 0;
export default class Dep {
... 省略
}
Dep.target = null; // 靜態(tài)變量,全局唯一
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
const targetStack = [];
export function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1]; // 賦值為棧頂元素
}
然后 Watcher 中,執(zhí)行函數(shù)之前進行入棧,執(zhí)行后進行出棧。
import { pushTarget, popTarget } from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.depIds = new Set(); // 擁有 has 函數(shù)可以判斷是否存在某個 id
this.deps = [];
this.newDeps = []; // 記錄新一次的依賴
this.newDepIds = new Set();
this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
/************修改的地方*******************************/
pushTarget(this); // 保存包裝了當前正在執(zhí)行的函數(shù)的 Watcher
/*******************************************/
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
/************修改的地方*******************************/
popTarget();
/*******************************************/
this.cleanupDeps();
}
return value;
}
...
}
測試
回到開頭的場景,再來執(zhí)行一下:
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
inner: "內(nèi)部",
};
observe(data);
const updateMyComponent = () => {
console.log("子組件收到:", data.inner);
};
const updateParentComponent = () => {
new Watcher(updateMyComponent);
console.log("父組件收到:", data.text);
};
new Watcher(updateParentComponent);
data.text = "hello, liang";
執(zhí)行 new Watcher(updateParentComponent); 的時候將 Watcher 入棧。
進入 updateParentComponent 函數(shù),執(zhí)行 new Watcher(updateMyComponent); 的時候將 Watcher 入棧。
執(zhí)行 updateMyComponent 函數(shù),data.inner 收集當前 Dep.target ,執(zhí)行完畢后 Watcher 出棧。
繼續(xù)執(zhí)行 updateParentComponent 函數(shù),data.text 收集當前 Dep.target 。
此時依賴就變得正常了,data.text 會觸發(fā) updateParentComponent 函數(shù),從而輸出如下:
子組件收到: 內(nèi)部
父組件收到:hello, world
子組件收到: 內(nèi)部
父組件收到:hello, liang
總結
今天這個相對好理解一些,通過棧解決了嵌套調(diào)用的情況。