看不懂來打我!讓性能提升56%的Vue3.5響應(yīng)式重構(gòu)
前言
在Vue3.5版本中最大的改動(dòng)就是響應(yīng)式重構(gòu),重構(gòu)后性能竟然炸裂的提升了56%。之所以重構(gòu)后的響應(yīng)式性能提升幅度有這么大,主要還是歸功于:雙向鏈表和版本計(jì)數(shù)。這篇文章我們來講講使用雙向鏈表后,Vue內(nèi)部是如何實(shí)現(xiàn)依賴收集和依賴觸發(fā)的。
3.5版本以前的響應(yīng)式
在Vue3.5以前的響應(yīng)式中主要有兩個(gè)角色:Sub(訂閱者)、Dep(依賴)。其中的訂閱者有watchEffect、watch、render函數(shù)、computed等。依賴有ref、reactive等響應(yīng)式變量。
舉個(gè)例子:
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {
dummy1 = counter1.value + counter2.value;
console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {
dummy2 = counter1.value + counter2.value + 1;
console.log("dummy2", dummy2);
});
counter1.value++;
counter2.value++;
</script>
在上面的兩個(gè)watchEffect中都會(huì)去監(jiān)聽ref響應(yīng)式變量:counter1和counter2。
初始化時(shí)會(huì)分別執(zhí)行這兩個(gè)watchEffect中的回調(diào)函數(shù),所以就會(huì)對(duì)里面的響應(yīng)式變量counter1和counter2進(jìn)行讀操作,所以就會(huì)走到響應(yīng)式變量的get攔截中。
在get攔截中會(huì)進(jìn)行依賴收集(此時(shí)的Dep依賴分別是變量counter1和counter2)。
因?yàn)樵谝蕾囀占陂g是在執(zhí)行watchEffect中的回調(diào)函數(shù),所以依賴對(duì)應(yīng)的Sub訂閱者就是watchEffect。
由于這里有兩個(gè)watchEffect,所以這里有兩個(gè)Sub訂閱者,分別對(duì)應(yīng)這兩個(gè)watchEffect。
在上面的例子中,watchEffect監(jiān)聽了多個(gè)ref變量。也就是說,一個(gè)Sub訂閱者(也就是一個(gè)watchEffect)可以訂閱多個(gè)依賴。
ref響應(yīng)式變量counter1被多個(gè)watchEffect給監(jiān)聽。也就是說,一個(gè)Dep依賴(也就是counter1變量)可以被多個(gè)訂閱者給訂閱。
Sub訂閱者和Dep依賴他們兩的關(guān)系是多對(duì)多的關(guān)系?。?!
圖片
上面這個(gè)就是以前的響應(yīng)式模型。
新的響應(yīng)式模型
在Vue3.5版本新的響應(yīng)式中,Sub訂閱者和Dep依賴之間不再有直接的聯(lián)系,而是新增了一個(gè)Link作為橋梁。Sub訂閱者通過Link訪問到Dep依賴,同理Dep依賴也是通過Link訪問到Sub訂閱者。如下圖:
圖片
把上面這個(gè)圖看懂了,你就能理解Vue新的響應(yīng)式系統(tǒng)啦?,F(xiàn)在你直接看這個(gè)圖有可能看不懂,沒關(guān)系,等我講完后你就能看懂了。
首先從上圖中可以看到Sub訂閱者和Dep依賴之間沒有任何直接的連接關(guān)系了,也就是說Sub訂閱者不能直接訪問到Dep依賴,Dep依賴也不能直接訪問Sub訂閱者。
Dep依賴我們可以看作是X軸,Sub訂閱者可以看作是Y軸,這些Link就是坐標(biāo)軸上面的坐標(biāo)。
Vue響應(yīng)式系統(tǒng)的核心還是沒有變,只是多了一個(gè)Link,依然還是以前的那一套依賴收集和依賴觸發(fā)的流程。
在依賴收集的過程中就會(huì)畫出上面這個(gè)圖,這個(gè)不要急,我接下來會(huì)仔細(xì)去講圖是如何畫出來的。
那么依賴觸發(fā)的時(shí)候又是如何利用上面這種圖從而實(shí)現(xiàn)觸發(fā)依賴的呢?我們來看個(gè)例子。
上面的這張圖其實(shí)對(duì)應(yīng)的是我之前舉的例子:
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {
dummy1 = counter1.value + counter2.value;
console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {
dummy2 = counter1.value + counter2.value + 1;
console.log("dummy2", dummy2);
});
counter1.value++;
counter2.value++;
</script>
圖中的Dep1依賴對(duì)應(yīng)的就是變量counter1,Dep2依賴對(duì)應(yīng)的就是變量counter2。Sub1訂閱者對(duì)應(yīng)的就是第一個(gè)watchEffect函數(shù),Sub2訂閱者對(duì)應(yīng)的就是第二個(gè)watchEffect函數(shù)。
當(dāng)執(zhí)行counter1.value++時(shí),就會(huì)被變量counter1(也就是Dep1依賴)的set函數(shù)攔截。從上圖中可以看到Dep1依賴有個(gè)箭頭(對(duì)照表中的sub屬性)指向Link3,并且Link3也有一個(gè)箭頭(對(duì)照表中的sub屬性)指向Sub2。
前面我們講過了這個(gè)Sub2就是對(duì)應(yīng)的第二個(gè)watchEffect函數(shù),指向Sub2后我們就可以執(zhí)行Sub2中的依賴,也就是執(zhí)行第二個(gè)watchEffect函數(shù)。這就實(shí)現(xiàn)了counter1.value++變量改變后,重新執(zhí)行第二個(gè)watchEffect函數(shù)。
執(zhí)行了第二個(gè)watchEffect函數(shù)后我們發(fā)現(xiàn)Link3在Y軸上面還有一個(gè)箭頭(對(duì)照表中的preSub屬性)指向了Link1。同理Link1也有一個(gè)箭頭(對(duì)照表中的sub屬性)指向了Sub1。
前面我們講過了這個(gè)Sub1就是對(duì)應(yīng)的第一個(gè)watchEffect函數(shù),指向Sub1后我們就可以執(zhí)行Sub1中的依賴,也就是執(zhí)行第一個(gè)watchEffect函數(shù)。這就實(shí)現(xiàn)了counter1.value++變量改變后,重新執(zhí)行第一個(gè)watchEffect函數(shù)。
至此我們就實(shí)現(xiàn)了counter1.value++變量改變后,重新去執(zhí)行依賴他的兩個(gè)watchEffect函數(shù)。
我們此時(shí)再來回顧一下我們前面畫的新的響應(yīng)式模型圖,如下圖:
圖片
我們從這張圖來總結(jié)一下依賴觸發(fā)的的規(guī)則:
響應(yīng)式變量Dep1改變后,首先會(huì)指向Y軸(Sub訂閱者)的隊(duì)尾的Link節(jié)點(diǎn)。然后從Link節(jié)點(diǎn)可以直接訪問到Sub訂閱者,訪問到訂閱者后就可以觸發(fā)其依賴,這里就是重新執(zhí)行對(duì)應(yīng)的watchEffect函數(shù)。
接著就是順著Y軸的隊(duì)尾向隊(duì)頭移動(dòng),每移動(dòng)到一個(gè)新的Link節(jié)點(diǎn)都可以指向一個(gè)新的Dep依賴,在這里觸發(fā)其依賴就會(huì)重新指向?qū)?yīng)的watchEffect函數(shù)。
看到這里有的同學(xué)有疑問如果是Dep2對(duì)應(yīng)的響應(yīng)式變量改變后指向Link4,那這個(gè)Link4又是怎么指向Sub2的呢?他們中間不是還隔了一個(gè)Link3嗎?
每一個(gè)Link節(jié)點(diǎn)上面都有一個(gè)sub屬性直接指向Y軸上面的Sub依賴,所以這里的Link4有個(gè)箭頭(對(duì)照表中的sub屬性)可以直接指向Sub2,然后進(jìn)行依賴觸發(fā)。
這就是Vue3.5版本使用雙向鏈表改進(jìn)后的依賴觸發(fā)原理,接下來我們會(huì)去講依賴收集過程中是如何將上面的模型圖畫出來的。
Dep、Sub和Link
在講Vue3.5版本依賴收集之前,我們先來了解一下新的響應(yīng)式系統(tǒng)中主要的三個(gè)角色:Dep依賴、Sub訂閱者、Link節(jié)點(diǎn)。
這三個(gè)角色其實(shí)都是class類,依賴收集和依賴觸發(fā)的過程中實(shí)際就是在操作這些類new出來的的對(duì)象。
我們接下來看看這些類中有哪些屬性和方法,其實(shí)在前面的響應(yīng)式模型圖中我們已經(jīng)使用箭頭標(biāo)明了這些類上面的屬性。
Dep依賴
簡化后的Dep類定義如下:
class Dep {
// 指向Link鏈表的尾部節(jié)點(diǎn)
subs: Link
// 收集依賴
track: Function
// 觸發(fā)依賴
trigger: Function
}
Dep依賴上面的subs屬性就是指向隊(duì)列的尾部,也就是隊(duì)列中最后一個(gè)Sub訂閱者對(duì)應(yīng)的Link節(jié)點(diǎn)。
圖片
比如這里的Dep1,豎向的Link1和Link3就組成了一個(gè)隊(duì)列。其中Link3是隊(duì)列的隊(duì)尾,Dep1的subs屬性就是指向Link3。
其次就是track函數(shù),對(duì)響應(yīng)式變量進(jìn)行讀操作時(shí)會(huì)觸發(fā)。觸發(fā)這個(gè)函數(shù)后會(huì)進(jìn)行依賴收集,后面我會(huì)講。
同樣trigger函數(shù)用于依賴觸發(fā),對(duì)響應(yīng)式變量進(jìn)行寫操作時(shí)會(huì)觸發(fā),后面我也會(huì)講。
Sub訂閱者
簡化后的Sub訂閱者定義如下:
interface Subscriber {
// 指向Link鏈表的頭部節(jié)點(diǎn)
deps: Link
// 指向Link鏈表的尾部節(jié)點(diǎn)
depsTail: Link
// 執(zhí)行依賴
notify: Function
}
想必細(xì)心的你發(fā)現(xiàn)了這里的Subscriber是一個(gè)interface接口,而不是一個(gè)class類。因?yàn)閷?shí)現(xiàn)了這個(gè)Subscriber接口的class類都是訂閱者,比如watchEffect、watch、render函數(shù)、computed等。
圖片
比如這里的Sub1,橫向的Link1和Link2就組成一個(gè)隊(duì)列。其中的隊(duì)尾就是Link2(depsTail屬性),隊(duì)頭就是Link1(deps屬性)。
還有就是notify函數(shù),執(zhí)行這個(gè)函數(shù)就是在執(zhí)行依賴。比如對(duì)于watchEffect來說,執(zhí)行notify函數(shù)后就會(huì)執(zhí)行watchEffect的回調(diào)函數(shù)。
Link節(jié)點(diǎn)
簡化后的Link節(jié)點(diǎn)類定義如下:
class Link {
// 指向Subscriber訂閱者
sub: Subscriber
// 指向Dep依賴
dep: Dep
// 指向Link鏈表的后一個(gè)節(jié)點(diǎn)(X軸)
nextDep: Link
// 指向Link鏈表的前一個(gè)節(jié)點(diǎn)(X軸)
prevDep: Link
// 指向Link鏈表的下一個(gè)節(jié)點(diǎn)(Y軸)
nextSub: Link
// 指向Link鏈表的上一個(gè)節(jié)點(diǎn)(Y軸)
prevSub: Link
}
前面我們講過了新的響應(yīng)式模型中Dep依賴和Sub訂閱者之間不會(huì)再有直接的關(guān)聯(lián),而是通過Link作為橋梁。
那么作為橋梁的Link節(jié)點(diǎn)肯定需要有兩個(gè)屬性能夠讓他直接訪問到Dep依賴和Sub訂閱者,也就是sub和dep屬性。
其中的sub屬性是指向Sub訂閱者,dep屬性是指向Dep依賴。
圖片
我們知道Link是坐標(biāo)軸的點(diǎn),那這個(gè)點(diǎn)肯定就會(huì)有上、下、左、右四個(gè)方向。
比如對(duì)于Link1可以使用nextDep屬性來訪問后面這個(gè)節(jié)點(diǎn)Link2,Link2可以使用prevDep屬性來訪問前面這個(gè)節(jié)點(diǎn)Link1。
請(qǐng)注意,這里名字雖然叫nextDep和prevDep,但是他們指向的卻是Link節(jié)點(diǎn)。然后通過這個(gè)Link節(jié)點(diǎn)的dep屬性,就可以訪問到后一個(gè)Dep依賴或者前一個(gè)Dep依賴。
同理對(duì)于Link1可以使用nextSub訪問后面這個(gè)節(jié)點(diǎn)Link3,Link3可以使用prevSub訪問前面這個(gè)節(jié)點(diǎn)Link1。
同樣的這里名字雖然叫nextSub和prevSub,但是他們指向的卻是Link節(jié)點(diǎn)。然后通過這個(gè)Link節(jié)點(diǎn)的sub屬性,就可以訪問到下一個(gè)Sub訂閱者或者上一個(gè)Sub訂閱者。
如何收集依賴
搞清楚了新的響應(yīng)式模型中的三個(gè)角色:Dep依賴、Sub訂閱者、Link節(jié)點(diǎn),我們現(xiàn)在就可以開始搞清楚新的響應(yīng)式模型是如何收集依賴的。
接下來我將會(huì)帶你如何一步步的畫出前面講的那張新的響應(yīng)式模型圖。
還是我們前面的那個(gè)例子,代碼如下:
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {
dummy1 = counter1.value + counter2.value;
console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {
dummy2 = counter1.value + counter2.value + 1;
console.log("dummy2", dummy2);
});
counter1.value++;
counter2.value++;
</script>
大家都知道響應(yīng)式變量有g(shù)et和set攔截,當(dāng)對(duì)變量進(jìn)行讀操作時(shí)會(huì)走到get攔截中,進(jìn)行寫操作時(shí)會(huì)走到set攔截中。
上面的例子第一個(gè)watchEffect我們叫做Sub1訂閱者,第二個(gè)watchEffect叫做Sub2訂閱者.
初始化時(shí)watchEffect中的回調(diào)會(huì)執(zhí)行一次,這里有兩個(gè)watchEffect,會(huì)依次去執(zhí)行。
在Vue內(nèi)部有個(gè)全局變量叫activeSub,里面存的是當(dāng)前active的Sub訂閱者。
執(zhí)行第一個(gè)watchEffect回調(diào)時(shí),當(dāng)前的activeSub就是Sub1。
在Sub1中使用到了響應(yīng)式變量counter1和counter2,所以會(huì)對(duì)這兩個(gè)變量依次進(jìn)行讀操作。
第一個(gè)watchEffect對(duì)counter1進(jìn)行讀操作
先對(duì)counter1進(jìn)行讀操作時(shí),會(huì)走到get攔截中。核心代碼如下:
class RefImpl {
get value() {
this.dep.track();
return this._value;
}
}
從上面可以看到在get攔截中直接調(diào)用了dep依賴的track方法進(jìn)行依賴收集。
在執(zhí)行track方法之前我們思考一下當(dāng)前響應(yīng)式系統(tǒng)中有哪些角色,分別是Sub1和Sub2這兩個(gè)watchEffect回調(diào)函數(shù)訂閱者,以及counter1和counter2這兩個(gè)Dep依賴。此時(shí)的響應(yīng)式模型如下圖:
圖片
從上圖可以看到此時(shí)只有X坐標(biāo)軸的Dep依賴,以及Y坐標(biāo)軸的Sub訂閱者,沒有一個(gè)Link節(jié)點(diǎn)。
我們接著來看看dep依賴的track方法,核心代碼如下:
class Dep {
// 指向Link鏈表的尾部節(jié)點(diǎn)
subs: Link;
track() {
let link = new Link(activeSub, this);
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link;
} else {
link.prevDep = activeSub.depsTail;
activeSub.depsTail!.nextDep = link;
activeSub.depsTail = link;
}
addSub(link);
}
}
從上面的代碼可以看到,每執(zhí)行一次track方法,也就是說每次收集依賴,都會(huì)執(zhí)行new Link去生成一個(gè)Link節(jié)點(diǎn)。
并且傳入兩個(gè)參數(shù),activeSub為當(dāng)前active的訂閱者,在這里就是Sub1(第一個(gè)watchEffect)。第二個(gè)參數(shù)為this,指向當(dāng)前的Dep依賴對(duì)象,也就是Dep1(counter1變量)。
先不看track后面的代碼,我們來看看Link這個(gè)class的代碼,核心代碼如下:
class Link {
// 指向Link鏈表的后一個(gè)節(jié)點(diǎn)(X軸)
nextDep: Link;
// 指向Link鏈表的前一個(gè)節(jié)點(diǎn)(X軸)
prevDep: Link;
// 指向Link鏈表的下一個(gè)節(jié)點(diǎn)(Y軸)
nextSub: Link;
// 指向Link鏈表的上一個(gè)節(jié)點(diǎn)(Y軸)
prevSub: Link;
- constructor(public sub: Subscriber, public dep: Dep) {
// ...省略
}
}
細(xì)心的小伙伴可能發(fā)現(xiàn)了在Link中沒有聲明sub和dep屬性,那么為什么前面我們會(huì)說Link節(jié)點(diǎn)中的sub和dep屬性分別指向Sub訂閱者和Dep依賴呢?
因?yàn)樵赾onstructor構(gòu)造函數(shù)中使用了public關(guān)鍵字,所以sub和dep就作為屬性暴露出來了。
執(zhí)行完let link = new Link(activeSub, this)后,在響應(yīng)式系統(tǒng)模型中初始化出來第一個(gè)Link節(jié)點(diǎn),如下圖:
圖片
從上圖可以看到Link1的sub屬性指向Sub1訂閱者,dep屬性指向Dep1依賴。
我們接著來看track方法中剩下的代碼,如下:
class Dep {
// 指向Link鏈表的尾部節(jié)點(diǎn)
subs: Link;
track() {
let link = new Link(activeSub, this);
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link;
} else {
link.prevDep = activeSub.depsTail;
activeSub.depsTail!.nextDep = link;
activeSub.depsTail = link;
}
addSub(link);
}
}
先來看if (!activeSub.deps),activeSub前面講過了是Sub1。activeSub.deps就是Sub1的deps屬性,也就是Sub1隊(duì)列上的第一個(gè)Link。
從上圖中可以看到此時(shí)的Sub1并沒有箭頭指向Link1,所以if (!activeSub.deps)為true,代碼會(huì)執(zhí)行
activeSub.deps = activeSub.depsTail = link;
deps和depsTail屬性分別指向Sub1隊(duì)列的頭部和尾部,當(dāng)前隊(duì)列中只有Link1這一個(gè)節(jié)點(diǎn),那么頭部和尾部當(dāng)然都指向Link1。
執(zhí)行完這行代碼后響應(yīng)式模型圖就變成下面這樣的了,如下圖:
圖片
從上圖中可以看到Sub1的隊(duì)列中只有Link1這一個(gè)節(jié)點(diǎn),所以隊(duì)列的頭部和尾部都指向Link1。
處理完Sub1的隊(duì)列,但是Dep1的隊(duì)列還沒處理,Dep1的隊(duì)列是由addSub(link)函數(shù)處理的。addSub函數(shù)代碼如下:
function addSub(link: Link) {
const currentTail = link.dep.subs;
if (currentTail !== link) {
link.prevSub = currentTail;
if (currentTail) currentTail.nextSub = link;
}
link.dep.subs = link;
}
由于Dep1隊(duì)列中沒有Link節(jié)點(diǎn),所以此時(shí)在addSub函數(shù)中主要是執(zhí)行第三塊代碼:link.dep.subs = link。`
link.dep是指向Dep1,前面我們講過了Dep依賴的subs屬性指向隊(duì)列的尾部。所以link.dep.subs = link就是將Link1指向Dep1的隊(duì)列的尾部,執(zhí)行完這行代碼后響應(yīng)式模型圖就變成下面這樣的了,如下圖:
圖片
到這里對(duì)第一個(gè)響應(yīng)式變量counter1進(jìn)行讀操作進(jìn)行的依賴收集就完了。
第一個(gè)watchEffect對(duì)counter2進(jìn)行讀操作
在第一個(gè)watchEffect中接著會(huì)對(duì)counter2變量進(jìn)行讀操作。同樣會(huì)走到get攔截中,然后執(zhí)行track函數(shù),代碼如下:
class Dep {
// 指向Link鏈表的尾部節(jié)點(diǎn)
subs: Link;
track() {
let link = new Link(activeSub, this);
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link;
} else {
link.prevDep = activeSub.depsTail;
activeSub.depsTail!.nextDep = link;
activeSub.depsTail = link;
}
addSub(link);
}
}
同樣的會(huì)執(zhí)行一次new Link(activeSub, this),然后把新生成的Link2的sub和dep屬性分別指向Sub1和Dep2。執(zhí)行后的響應(yīng)式模型圖如下圖:
圖片
從上面的圖中可以看到此時(shí)Sub1的deps屬性是指向Link1的,所以這次代碼會(huì)走進(jìn)else模塊中。else部分代碼如下:
link.prevDep = activeSub.depsTail;
activeSub.depsTail.nextDep = link;
activeSub.depsTail = link;
activeSub.depsTail指向Sub1隊(duì)列尾部的Link,值是Link1。所以執(zhí)行l(wèi)ink.prevDep = activeSub.depsTail就是將Link2的prevDep屬性指向Link1。
同理activeSub.depsTail.nextDep = link就是將Link1的nextDep屬性指向Link2,執(zhí)行完這兩行代碼后Link1和Link2之間就建立關(guān)系了。如下圖:
圖片
從上圖中可以看到此時(shí)Link1和Link2之間就有箭頭連接,可以互相訪問到對(duì)方。
最后就是執(zhí)行activeSub.depsTail = link,這行代碼是將Sub1隊(duì)列的尾部指向Link2。執(zhí)行完這行代碼后模型圖如下:
圖片
Sub1訂閱者的隊(duì)列就處理完了,接著就是處理Dep2依賴的隊(duì)列。Dep2的處理方式和Dep1是一樣的,讓Dep2隊(duì)列的隊(duì)尾指向Link2,處理完了后模型圖如下:
圖片
到這里第一個(gè)watchEffect(也就是Sub1)對(duì)其依賴的兩個(gè)響應(yīng)式變量counter1(也就是Dep1)和counter2(也就是Dep2),進(jìn)行依賴收集的過程就執(zhí)行完了。
第二個(gè)watchEffect對(duì)counter1進(jìn)行讀操作
接著我們來看第二個(gè)watchEffect,同樣的還是會(huì)對(duì)counter1進(jìn)行讀操作。然后觸發(fā)其get攔截,接著執(zhí)行track方法?;貞浺幌聇rack方法的代碼,如下:
class Dep {
// 指向Link鏈表的尾部節(jié)點(diǎn)
subs: Link;
track() {
let link = new Link(activeSub, this);
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link;
} else {
link.prevDep = activeSub.depsTail;
activeSub.depsTail!.nextDep = link;
activeSub.depsTail = link;
}
addSub(link);
}
}
這里還是會(huì)使用new Link(activeSub, this)創(chuàng)建一個(gè)Link3節(jié)點(diǎn),節(jié)點(diǎn)的sub和dep屬性分別指向Sub2和Dep1。如下圖:
圖片
同樣的Sub2隊(duì)列上此時(shí)還沒任何值,所以if (!activeSub.deps)為true,和之前一樣會(huì)去執(zhí)行activeSub.deps = activeSub.depsTail = link;將Sub2隊(duì)列的頭部和尾部都設(shè)置為Link3。如下圖:
圖片
處理完Sub2隊(duì)列后就應(yīng)該調(diào)用addSub函數(shù)來處理Dep1的隊(duì)列了,回憶一下addSub函數(shù),代碼如下:
function addSub(link: Link) {
const currentTail = link.dep.subs;
if (currentTail !== link) {
link.prevSub = currentTail;
if (currentTail) currentTail.nextSub = link;
}
link.dep.subs = link;
}
link.dep指向Dep1依賴,link.dep.subs指向Dep1依賴隊(duì)列的尾部。從前面的圖可以看到此時(shí)隊(duì)列的尾部是Link1,所以currentTail的值就是Link1。
if (currentTail !== link)也就是判斷Link1和Link3是否相等,很明顯不相等,就會(huì)走到if的里面去。
接著就是執(zhí)行l(wèi)ink.prevSub = currentTail,前面講過了此時(shí)link就是Link3,currentTail就是Link1。執(zhí)行這行代碼就是將Link3的prevSub屬性指向Link1。
接著就是執(zhí)行currentTail.nextSub = link,這行代碼是將Link1的nextSub指向Link3。
執(zhí)行完上面這兩行代碼后Link1和Link3之間就建立聯(lián)系了,可以通過prevSub和nextSub屬性訪問到對(duì)方。如下圖:
圖片
接著就是執(zhí)行l(wèi)ink.dep.subs = link,將Dep1隊(duì)列的尾部指向Link3,如下圖:
圖片
到這里第一個(gè)響應(yīng)式變量counter1進(jìn)行依賴收集就完成了。
第二個(gè)watchEffect對(duì)counter2進(jìn)行讀操作
在第二個(gè)watchEffect中接著會(huì)對(duì)counter2變量進(jìn)行讀操作。同樣會(huì)走到get攔截中,然后執(zhí)行track函數(shù),代碼如下:
class Dep {
// 指向Link鏈表的尾部節(jié)點(diǎn)
subs: Link;
track() {
let link = new Link(activeSub, this);
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link;
} else {
link.prevDep = activeSub.depsTail;
activeSub.depsTail!.nextDep = link;
activeSub.depsTail = link;
}
addSub(link);
}
}
這里還是會(huì)使用new Link(activeSub, this)創(chuàng)建一個(gè)Link4節(jié)點(diǎn),節(jié)點(diǎn)的sub和dep屬性分別指向Sub2和Dep2。如下圖:
圖片
此時(shí)的activeSub就是Sub2,activeSub.deps就是指向Sub2隊(duì)列的頭部。所以此時(shí)頭部是指向Link3,代碼會(huì)走到else模塊中。
在else中首先會(huì)執(zhí)行l(wèi)ink.prevDep = activeSub.depsTail,activeSub.depsTail是指向Sub2隊(duì)列的尾部,也就是Link3。執(zhí)行完這行代碼后會(huì)將Link4的prevDep指向Link3。
接著就是執(zhí)行activeSub.depsTail!.nextDep = link,前面講過了activeSub.depsTail是指向Link3。執(zhí)行完這行代碼后會(huì)將Link3的nextDep屬性指向Link4。
執(zhí)行完上面這兩行代碼后Link3和Link4之間就建立聯(lián)系了,可以通過nextDep和prevDep屬性訪問到對(duì)方。如下圖:
圖片
接著就是執(zhí)行activeSub.depsTail = link,將Sub2隊(duì)列的尾部指向Link4。如下圖:
圖片
接著就是執(zhí)行addSub函數(shù)處理Dep2的隊(duì)列,代碼如下:
function addSub(link: Link) {
const currentTail = link.dep.subs;
if (currentTail !== link) {
link.prevSub = currentTail;
if (currentTail) currentTail.nextSub = link;
}
link.dep.subs = link;
}
link.dep指向Dep2依賴,link.dep.subs指向Dep2依賴隊(duì)列的尾部。從前面的圖可以看到此時(shí)隊(duì)列的尾部是Link2,所以currentTail的值就是Link2。前面講過了此時(shí)link就是Link4,if (currentTail !== link)也就是判斷Link2和Link4是否相等,很明顯不相等,就會(huì)走到if的里面去。
接著就是執(zhí)行l(wèi)ink.prevSub = currentTail,currentTail就是Link2。執(zhí)行這行代碼就是將Link4的prevSub屬性指向Link2。
接著就是執(zhí)行currentTail.nextSub = link,這行代碼是將Link2的nextSub指向Link4。
執(zhí)行完上面這兩行代碼后Link2和Link4之間就建立聯(lián)系了,可以通過prevSub和nextSub屬性訪問到對(duì)方。如下圖:
圖片
最后就是執(zhí)行l(wèi)ink.dep.subs = link將Dep2隊(duì)列的尾部指向Link4,如下圖:
圖片
至此整個(gè)依賴收集過程就完成了,最終就畫出了Vue新的響應(yīng)式模型。
依賴觸發(fā)
當(dāng)執(zhí)行counter1.value++時(shí),就會(huì)被變量counter1(也就是Dep1依賴)的set函數(shù)攔截。
此時(shí)就可以通過Dep1的subs屬性指向隊(duì)列的尾部,也就是指向Link3。
Link3中可以直接通過sub屬性訪問到訂閱者Sub2,也就是第二個(gè)watchEffect,從而執(zhí)行第二個(gè)watchEffect的回調(diào)函數(shù)。
接著就是使用Link的preSub屬性從隊(duì)尾依次移動(dòng)到隊(duì)頭,從而觸發(fā)Dep1隊(duì)列中的所有Sub訂閱者。
在這里就是使用preSub屬性訪問到Link1(就到隊(duì)列的頭部啦),Link1中可以直接通過sub屬性訪問到訂閱者Sub1,也就是第一個(gè)watchEffect,從而執(zhí)行第一個(gè)watchEffect的回調(diào)函數(shù)。
總結(jié)
這篇文章講了Vue新的響應(yīng)式模型,里面主要有三個(gè)角色:Dep依賴、Sub訂閱者、Link節(jié)點(diǎn)。
Dep依賴和Sub訂閱者不再有直接的聯(lián)系,而是通過Link節(jié)點(diǎn)作為橋梁。
依賴收集的過程中會(huì)構(gòu)建Dep依賴的隊(duì)列,隊(duì)列是由Link節(jié)點(diǎn)組成。以及構(gòu)建Sub訂閱者的隊(duì)列,隊(duì)列同樣是由Link節(jié)點(diǎn)組成。
依賴觸發(fā)時(shí)就可以通過Dep依賴的隊(duì)列的隊(duì)尾出發(fā),Link節(jié)點(diǎn)可以訪問和觸發(fā)對(duì)應(yīng)的Sub訂閱者。
然后依次從隊(duì)尾向隊(duì)頭移動(dòng),依次觸發(fā)隊(duì)列中每個(gè)Link節(jié)點(diǎn)的Sub訂閱者。