Vue2剝絲抽繭-響應(yīng)式系統(tǒng)之分支切換
場景
我們考慮一下下邊的代碼會(huì)輸出什么。
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
ok: true,
};
observe(data);
const updateComponent = () => {
console.log("收到", data.ok ? data.text : "not");
};
new Watcher(updateComponent); // updateComponent 執(zhí)行一次函數(shù),輸出 hello, world
data.ok = false; // updateComponent 執(zhí)行一次函數(shù),輸出 not
data.text = "hello, liang"; // updateComponent 會(huì)執(zhí)行嗎?
我們來一步一步理清:
observer(data)
攔截了 data 中 text 和 ok 的 get、set,并且各自初始化了一個(gè) Dep 實(shí)例,用來保存依賴它們的 Watcher 對(duì)象。
new Watcher(updateComponent);
這一步會(huì)執(zhí)行 updateComponent 函數(shù),執(zhí)行過程中用到的所有對(duì)象屬性,會(huì)將 Watcher 收集到相應(yīng)對(duì)象屬性中的Dep 中。
當(dāng)然這里的 Watcher 其實(shí)是同一個(gè),所以用了指向的箭頭。
data.ok = false;
這一步會(huì)觸發(fā) set ,從而執(zhí)行 Dep 中所有的 Watcher ,此時(shí)就會(huì)執(zhí)行一次 updateComponent 。
執(zhí)行 updateComponent 就會(huì)重新讀取 data 中的屬性,觸發(fā) get,然后繼續(xù)收集 Watcher 。
重新執(zhí)行 updateComponent 函數(shù) 的時(shí)候:
const updateComponent = () => {
console.log("收到", data.ok ? data.text : "not");
};
因?yàn)?data.ok 的值變?yōu)?false ,所以就不會(huì)觸發(fā) data.text 的 get ,text 的 Dep 就不會(huì)變化了。
而 data.ok 會(huì)繼續(xù)執(zhí)行,觸發(fā) get 收集 Watcher ,但由于我們 Dep 中使用的是數(shù)組,此時(shí)收集到的兩個(gè) Wacher 其實(shí)是同一個(gè),這里是有問題,會(huì)導(dǎo)致 updateComponent 重復(fù)執(zhí)行,一會(huì)兒我們來解決下。
data.text = "hello, liang";
執(zhí)行這句的時(shí)候,會(huì)觸發(fā) text 的 set,所以會(huì)執(zhí)行一次 updateComponent 。但從代碼來看 updateComponent 函數(shù)中由于 data.ok 為 false,data.text 對(duì)輸出沒有任何影響,這次執(zhí)行其實(shí)是沒有必要的。
之所以執(zhí)行了,是因?yàn)榈谝淮螆?zhí)行 updateComponent 讀取了 data.text 從而收集了 Watcher ,第二次執(zhí)行 updateComponent 的時(shí)候,data.text 雖然沒有讀到,但之前的 Watcher 也沒有清除掉,所以這一次改變 data.text 的時(shí)候 updateComponent 依舊會(huì)執(zhí)行。
所以我們需要的就是當(dāng)重新執(zhí)行 updateComponent 的時(shí)候,如果 Watcher 已經(jīng)不依賴于某個(gè) Dep 了,我們需要將當(dāng)前 Watcher 從該 Dep 中移除掉。
問題
總結(jié)下來我們需要做兩件事情。
- 去重,Dep 中不要重復(fù)收集 Watcher 。
- 重置,如果該屬性對(duì) Dep 中的 Wacher 已經(jīng)沒有影響了(換句話就是,Watcher 中的 updateComponent 已經(jīng)不會(huì)讀取到該屬性了 ),就將該 Watcher 從該屬性的 Dep 中刪除。
去重
去重的話有兩種方案:
- Dep 中的 subs 數(shù)組換為 Set。
- 每個(gè) Dep 對(duì)象引入 id ,Watcher 對(duì)象中記錄所有的 Dep 的 id,下次重新收集依賴的時(shí)候,如果 Dep 的 id 已經(jīng)存在,就不再收集該 Watcher 了。
Vue2 源碼中采用的是方案 2 這里我們實(shí)現(xiàn)下:
Dep 類的話只需要引入 id 即可。
/*************改動(dòng)***************************/
let uid = 0;
/****************************************/
export default class Dep {
static target; //當(dāng)前在執(zhí)行的函數(shù)
subs; // 依賴的函數(shù)
id; // Dep 對(duì)象標(biāo)識(shí)
constructor() {
/**************改動(dòng)**************************/
this.id = uid++;
/****************************************/
this.subs = []; // 保存所有需要執(zhí)行的函數(shù)
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 委托給 Dep.target 去調(diào)用 addSub
Dep.target.addDep(this);
}
}
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
Dep.target = null; // 靜態(tài)變量,全局唯一
在 Watcher 中,我們引入 this.depIds 來記錄所有的 id 。
import Dep from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
/*************改動(dòng)***************************/
this.depIds = new Set(); // 擁有 has 函數(shù)可以判斷是否存在某個(gè) id
/****************************************/
this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
Dep.target = this; // 保存包裝了當(dāng)前正在執(zhí)行的函數(shù)的 Watcher
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
this.cleanupDeps();
}
return value;
}
/**
* Add a dependency to this directive.
*/
addDep(dep) {
/*************改動(dòng)***************************/
const id = dep.id;
if (!this.depIds.has(id)) {
dep.addSub(this);
}
/****************************************/
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
this.run();
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
this.get();
}
}
重置
同樣是兩個(gè)方案:
- 全量式移除,保存 Watcher 所影響的所有 Dep 對(duì)象,當(dāng)重新收集 Watcher 的前,把當(dāng)前 Watcher 從記錄中的所有 Dep 對(duì)象中移除。
- 增量式移除,重新收集依賴時(shí),用一個(gè)新的變量記錄所有的 Dep 對(duì)象,之后再和舊的 Dep 對(duì)象列表比對(duì),如果新的中沒有,舊的中有,就將當(dāng)前 Watcher 從該 Dep 對(duì)象中移除。
Vue2 中采用的是方案 2,這里也實(shí)現(xiàn)下。
首先是 Dep 類,我們需要提供一個(gè) removeSub 方法。
import { remove } from "./util";
/*
export function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
*/
let uid = 0;
export default class Dep {
static target; //當(dāng)前在執(zhí)行的函數(shù)
subs; // 依賴的函數(shù)
id; // Dep 對(duì)象標(biāo)識(shí)
constructor() {
this.id = uid++;
this.subs = []; // 保存所有需要執(zhí)行的函數(shù)
}
addSub(sub) {
this.subs.push(sub);
}
/*************新增************************/
removeSub(sub) {
remove(this.subs, sub);
}
/****************************************/
depend() {
if (Dep.target) {
// 委托給 Dep.target 去調(diào)用 addSub
Dep.target.addDep(this);
}
}
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
Dep.target = null; // 靜態(tài)變量,全局唯一
然后是 Watcher 類,我們引入 this.deps 來保存所有的舊 Dep 對(duì)象,引入 this.newDeps 來保存所有的新 Dep 對(duì)象。
import Dep from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.depIds = new Set(); // 擁有 has 函數(shù)可以判斷是否存在某個(gè) id
/*************新增************************/
this.deps = [];
this.newDeps = []; // 記錄新一次的依賴
this.newDepIds = new Set();
/****************************************/
this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
Dep.target = this; // 保存包裝了當(dāng)前正在執(zhí)行的函數(shù)的 Watcher
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
/*************新增************************/
this.cleanupDeps();
/****************************************/
}
return value;
}
/**
* Add a dependency to this directive.
*/
addDep(dep) {
const id = dep.id;
/*************新增************************/
// 新的依賴已經(jīng)存在的話,同樣不需要繼續(xù)保存
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
/****************************************/
}
/**
* Clean up for dependency collection.
*/
/*************新增************************/
cleanupDeps() {
let i = this.deps.length;
// 比對(duì)新舊列表,找到舊列表里有,但新列表里沒有,來移除相應(yīng) Watcher
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
// 新的列表賦值給舊的,新的列表清空
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
/****************************************/
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
this.run();
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
this.get();
}
}
測(cè)試
回到開頭的代碼:
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
ok: true,
};
observe(data);
const updateComponent = () => {
console.log("收到", data.ok ? data.text : "not");
};
new Watcher(updateComponent); // updateComponent 執(zhí)行一次函數(shù),輸出 hello, world
data.ok = false; // updateComponent 執(zhí)行一次函數(shù),輸出 not
data.text = "hello, liang"; // updateComponent 會(huì)執(zhí)行嗎?
此時(shí) data.text 修改的話就不會(huì)再執(zhí)行 updateComponent 了,因?yàn)榈诙螆?zhí)行的時(shí)候,我們把 data.text 中 Dep 里的 Watcher 清除了。
總結(jié)
今天這個(gè)主要就是對(duì)響應(yīng)式系統(tǒng)的一點(diǎn)優(yōu)化,避免不必要的重新執(zhí)行。所做的事情就是重新調(diào)用函數(shù)的時(shí)候,把已經(jīng)沒有關(guān)聯(lián)的 Watcher 去除。
不知道看到這里大家有沒有一個(gè)疑問,我是一直沒想到說服我的點(diǎn),歡迎一起交流:
在解決去重問題上,我們是引入了 id ,但如果直接用 set 其實(shí)就可以。在 Watcher 類中是用 Set 來存 id ,用數(shù)組來存 Dep 對(duì)象,為什么不直接用 Set 來存 Dep 對(duì)象呢?