前言
Vue3.5版本又將響應式給重構了,重構後的響應式系統主要有兩部分組成: 雙向連結串列和 版本計數。我們在前兩篇文章中我們已經講過了 雙向連結串列和 版本計數,這篇文章我們來講講為什麼這次重構能夠讓記憶體佔用減少56%。
歐陽年底也要畢業了,加入歐陽的面試交流群(分享內推資訊)、高質量vue原始碼交流群
為什麼說“又”將響應式重構了
因為在之前的Vue3.4版本中剛剛將響應式給重構了,這次響應式重構是vscode外掛Vue-Official(原名Volar)的作者Johnson Chu搞的。
3.4版本的重構最佳化了很多東西,最直觀的就是:computed計算屬性的值沒有變化,另外一個watch又監聽了這個computed的值。在3.4以前還是會觸發watch的回撥,經過3.4的最佳化後就不會觸發了。
在3.5版本以前,Vue的響應式系統中有兩個角色:Sub訂閱者和Dep依賴。
Sub訂閱者
:主要有watchEffect、watch、render函式、computed等。
Dep依賴
:主要有ref、reactive、computed等響應式變數。
他們兩之間是相互依賴的關係,如下圖:
Dep依賴
(比如ref響應式變數)可以透過dep屬性
訪問到Sub訂閱者
(比如computed計算屬性),就知道了到底有哪些訂閱者依賴自己,當自己的值改變後就能去通知訂閱者。
同樣Sub訂閱者
(比如computed計算屬性)可以透過deps
屬性訪問到Dep依賴
(比如ref響應式變數),當Sub訂閱者
不再依賴某個變數時就可以透過這個關係去訪問到這個Dep依賴
。然後把自己從不再依賴的變數的Sub訂閱者
集合中去掉,這樣當這個響應式變數改變後就不會通知到不再訂閱到他的Sub訂閱者
了。
我們來看個例子,程式碼如下:
<template>
<p>{{ doubleCount }}</p>
<button @click="flag = !flag">切換flag</button>
</template>
<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);
const doubleCount = computed(() => {
console.log("computed");
if (flag.value) {
return count1.value * 2;
} else {
return count2.value * 2;
}
});
</script>
當flag
的值為true時計算屬性doubleCount
其實只依賴響應式變數flag
和count1
,當flag
的值切換為false時,計算屬性應該變成依賴變數flag
和count2
。
就上面這個更新Sub訂閱者
依賴的邏輯,Vue其實重構了很多次。在早期的Vue3版本中是直接清空Sub訂閱者
所依賴的響應式變數,然後再重新執行計算屬性doubleCount
時再去將新的響應式變數進行收集。很明顯這個版本記憶體的使用就非常浪費了。
在最新的Vue3.4版本重構後的響應式系統中會在執行計算屬性之前利用_trackId
和_depsLength
欄位進行標記,在重新執行計算屬性時進行依賴收集就可以利用_trackId
和_depsLength
欄位判斷出Dep依賴是否能夠複用,並且執行完計算屬性的回撥函式後同樣利用_trackId
和_depsLength
欄位就可以將不再依賴的Dep依賴給移除掉。
上面這個方案看著很完美,但是他的核心是依賴計算屬性中所依賴的變數順序不變,如果順序變了,那麼依然還是不能夠複用的,同樣會對浪費記憶體。(PS:這一段3.4版本響應式看不懂沒關係,因為他已經是過去式了)
記憶體最佳化主要原因:複用Link節點
在Vue3.5版本中那個最瞭解Vue的男人出手了,使用雙向連結串列
和版本計數
將響應式系統再次給重構了。說實話這次重構後讓讀響應式原始碼的門檻變得更高了,但是收益特別明顯,最主要是透過複用Link節點去實現減少記憶體的使用。
還是上面的那個例子,對應新的響應式模型如下圖:
在新的響應式模型中Sub訂閱者
和Dep依賴
之間不再有直接的關聯關係了,而是透過中間的Link節點
作為橋樑去關聯。
在前一節中我們講過了,3.5以前Sub訂閱者
中有屬性會去存依賴的Dep依賴
,Dep依賴
中有屬性去存依賴他的Sub訂閱者
,所以導致當Sub訂閱者
依賴的變數需要更新時就無法做到完全的複用,記憶體就會浪費。
如果下面的內容你看不懂,這不是你理解力有問題,原因是你對雙向連結串列不熟悉,可以先看看我之前的 雙向連結串列文章。
在3.5新的響應式模型中,X軸是Dep依賴
,Y軸是Sub訂閱者
,Link節點
是作為座標軸上面的點。每一組Dep依賴
和Sub訂閱者
都會對應一個Link節點,並且可以透過這個Link節點
直接訪問到Dep依賴
和Sub訂閱者
。
在Y軸上面找一個點(比如Sub1也就是計算屬性doubleCount
),橫向出發就可以找到Sub1訂閱者
所依賴的所有響應式變數。因為橫向的這些Link節點是一個雙向連結串列,並且可以透過某一個Link節點直接訪問到他的Dep依賴。
當flag
的值切換為false後,訂閱者Sub1
所依賴的響應式變數就從flag+count1
變成flag+count2
。這時我們需要做的事情就很簡單了,新建一個Link3
節點,可以直接訪問到Sub1
和Dep3
。然後將Link1
中原本指向Link2
的指標改為指向Link3
,同時讓Link3
的指標也指向Link1
。並且將Link2
指向Link1
的指標改為指向空
,由於Dep2
現在不被任何訂閱者所依賴了,所以將Link2
原本指向Dep2
的指標也改為指向空,同樣將Dep2
指向Link2
的指標也指向空。
上面的一頓操作,除了必要的初始化一個Link3
之外我們一直都是在進行指標的操作,並不像以前的響應式一樣去增加Sub訂閱者依賴或者減少依賴,這是非常高效的方式。
當flag
的值切換為false後,新的響應式模型圖如下:
從上圖中可以看到Link2
已經徹底從雙向連結串列中移除了,並且整個過程中我們都是在操作指標的指向,所以Link1
也一直都是複用的。
V8在進行垃圾回收的時候發現Link2
不再被任何變數所使用,就可以認為Link2
是一個可以被回收的變數,就會將其直接回收釋放記憶體。
Link節點複用以及讓不再使用的Link節點儘快的被回收進而釋放記憶體,就是這次響應式重構減少56%記憶體佔用的主要原因。
其他最佳化
有了雙向連結串列後依賴觸發也變得更加清晰了,當某個響應式變數改變後,只需要遍歷Dep依賴(縱向)的Link節點組成的雙向連結串列,然後透過這些Link節點直接訪問到對應的Sub訂閱者,觸發其依賴。
基於此Sub訂閱者的觸發就是一個線性的過程,所以就可以實現將需要觸發的Sub訂閱者串起來組成了一個Sub訂閱者組成的佇列。等需要觸發的訂閱者收集完了後,再去進行觸發Sub訂閱者,避免同一個訂閱者被觸發多次。
依賴觸發相比之前也變得更加簡單了,效能以及記憶體也有所提升。
最後就是因為有了雙向連結串列
和版本計數
的加持後,computed計算屬性變得更加聰明,現在是惰性計算了。computed計算屬性只有等有人使用他(比如在template中使用計算屬性doubleCount)後才會去執行計算屬性中的回撥函式,以及3.4版本中就已經實現的如果計算屬性值沒有變化,另外一個watch又監聽了這個computed的值,此時這個watch不會被觸發。關於這個可以看我之前的版本計數文章。
總結
Vue3.5響應式重構主要是透過雙向連結串列
和版本計數
實現的,最佳化後記憶體佔用減少了56%。主要原因是:在新的響應式系統中多了一個Link節點
用於連結Sub訂閱者
和Dep依賴
,更新Sub訂閱者依賴只是進行指標的變換,並且還能夠複用Link節點
以及將不再使用的Link節點
給孤立出來便於V8更快的將這個Link節點給回收。此外還有Sub訂閱者的觸發也變得更加簡單,以及現在是computed計算屬性是惰性計算了,這些最佳化同樣也最佳化了記憶體的使用。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。