「從0實現(xiàn)React18系列」Reconciler架構(gòu)的雙緩存樹實現(xiàn)原理

前言
通過??上一篇??文章的學(xué)習(xí),了解了Fiber是什么,知道了Fiber節(jié)點可以保存對應(yīng)的DOM節(jié)點。Fiber節(jié)點構(gòu)成的Fiber Tree會對應(yīng)DOM Tree。
前面也提到Fiber是一種新的調(diào)和算法,那么它是如何更新DOM節(jié)點的呢?
單個節(jié)點的創(chuàng)建更新流程
對于同一個節(jié)點,React 會比較這個節(jié)點的ReactElement與FiberNode,生成子FiberNode。并根據(jù)比較的結(jié)果生成不同標(biāo)記(插入、刪除、移動...),對應(yīng)不同宿主環(huán)境API的執(zhí)行。

根據(jù)上面的Reconciler的工作流程,舉一個例子:
比如:
mount階段,掛載<div></div>。
- 先通過jsx("div")生成 React Element <div></div>。
- 生成的對應(yīng)的fiberNode為null(由于是由于是掛載階段,React還未構(gòu)建組件樹)。
- 生成子fiberNode(實際上就是這個div的fiber節(jié)點)。
- 生成Placement標(biāo)記。
將<div></div>更新為<p></p>。
update階段,更新將<div></div>更新為<p></p>。
- 先通過jsx("p")生成 React Element <p></p>。
- p與對應(yīng)的fiberNode作比較(FiberNode {type: 'div'})。
- 生成子fiberNode為null。
- 生成對應(yīng)標(biāo)記Delement Placement。
用一張圖解釋上面的流程:

當(dāng)所有的ReactElement比較完后,會生成一顆fiberNode Tree,一共會存在兩棵fiberNode Tree。
- current:與視圖中真實UI對應(yīng)的fiberNode樹。
- workInProgress:觸發(fā)更新后,正在reconciler中計算的fiberNode Tree(用于下一次的視圖更新,在下一次視圖更新后,會變成current Tree)。
這就是React中的"雙緩存樹"技術(shù)。
什么是"雙緩存"?
雙緩存技術(shù)是一種計算機(jī)圖形學(xué)中用于減少屏幕閃爍和提高渲染性能的技術(shù)。
就好像你是一個畫家,你需要在一個畫布上繪制一幅畫。在沒有雙緩存技術(shù)的情況下,你會直接在畫布上作畫。當(dāng)你繪制一條線或一個形狀時,觀眾會立即看到這個過程。如果你的繪畫速度較慢,觀眾可能會看到畫面的閃爍和變化,這會導(dǎo)致視覺上的不舒適。
引入雙緩存技術(shù)就好比你有兩個畫布:一個是主畫布,觀眾可以看到它;另一個是隱藏畫布,觀眾看不到它。在這種情況下,你會在隱藏畫布上進(jìn)行繪畫。當(dāng)你完成一個階段性的繪制任務(wù)后,你將隱藏畫布上的圖像瞬間復(fù)制到主畫布上。觀眾只能看到主畫布上的圖像,而看不到隱藏畫布上的繪制過程。這樣,即使你的繪畫速度較慢,觀眾也不會看到畫面的閃爍和變化,從而獲得更流暢的視覺體驗。
使用雙緩存技術(shù)時,計算機(jī)會在一個隱藏的緩沖區(qū)(后臺緩沖區(qū))上進(jìn)行繪制,然后將繪制好的圖像一次性復(fù)制到屏幕上(前臺緩沖區(qū))。這樣可以減少屏幕閃爍,并提高渲染性能。
這種在內(nèi)存中構(gòu)建并直接替換的技術(shù)叫作雙緩存。
React 中使用"雙緩存"來完成Fiber Tree的構(gòu)建與替換,對應(yīng)著DOM Tree的創(chuàng)建于與更新。
雙緩存Fiber樹
Fiber架構(gòu)中同時存在兩棵Fiber Tree,一顆是"真實UI對應(yīng)的 Fiber Tree"可以理解為前緩沖區(qū)。另一課是"正在內(nèi)存中構(gòu)建的 Fiber Tree"可以理解為后緩沖區(qū),這里值宿主環(huán)境(比如瀏覽器)。
當(dāng)前屏幕上顯示內(nèi)容對應(yīng)的Fiber樹稱為current Fiber樹,正在內(nèi)存中構(gòu)建的Fiber樹稱為workInProgress Fiber樹。
current Fiber樹中的Fiber節(jié)點被稱為current fiber,workInProgress Fiber樹中的Fiber節(jié)點被稱為workInProgress fiber,他們通過alternate屬性連接。
雙緩存樹一個顯著的特點就是兩棵樹之間會互相切換,通過alternate屬性連接。
雙緩存樹切換的規(guī)則
React應(yīng)用的根節(jié)點通過current指針在不同F(xiàn)iber樹的HostRootFiber根節(jié)點(ReactDOM.render創(chuàng)建的根節(jié)點)間切換。
- 在 mount時(首次渲染),會根據(jù)jsx方法返回的React Element構(gòu)建Fiber對象,形成Fiber樹。
- 然后這棵Fiber樹會作為current Fiber應(yīng)用到真實DOM上。
- 在 update時(狀態(tài)更新),會根據(jù)狀態(tài)變更后的React Element和current Fiber作對比形成新的workInProgress Fiber樹。
- 即當(dāng)workInProgress Fiber樹構(gòu)建完成交給Renderer(渲染器)渲染在頁面上后,應(yīng)用根節(jié)點的current指針指向workInProgress Fiber樹。
- 然后workInProgress Fiber切換成current Fiber應(yīng)用到真實DOM上,這就達(dá)到了更新的目的。
這一切都是在內(nèi)存中發(fā)生的,從而減少了對DOM的直接操作。
每次狀態(tài)更新都會產(chǎn)生新的workInProgress Fiber樹,通過current與workInProgress的替換,完成DOM更新,這就是React中用的雙緩存樹切換規(guī)則。
Renderer 是一個與特定宿主環(huán)境(如瀏覽器 DOM、服務(wù)器端渲染、React Native 等)相關(guān)的模塊。Renderer 負(fù)責(zé)將 React 組件樹轉(zhuǎn)換為特定宿主環(huán)境下的實際 UI。從而使 React 能夠在多個平臺上運行。
上面的語言可能有些枯燥,我們來畫個圖演示一下。
比如有下面這樣一段代碼,點擊元素把div切換成p元素:

接下來,我們分別從 mount(首次渲染)和 update(更新)兩個角度講解 Fiber 架構(gòu)的工作原理。
mount 時 Fiber Tree的構(gòu)建
mount 時有兩種情況:
- 整個應(yīng)用的首次渲染,這種情況發(fā)生首次進(jìn)入頁面時。
- 某個組件的首次渲染,當(dāng) isShow 為 true時,Btn 組件進(jìn)入 mount 首次渲染流程。
假如有這樣一段代碼:
mount 時上面的Fiber樹構(gòu)建過程如下:
- 首次執(zhí)行ReactDOM.createRoot(root)會創(chuàng)建fiberRootNode。
- 接著執(zhí)行到render(<App />)時會創(chuàng)建HostRootFiber,實際上它是一個HostRoot節(jié)點。
fiberRootNode 是整個應(yīng)用的根節(jié)點,HostRootFiber 是 <App /> 所在組件樹的根節(jié)點。
- 從HostRootFiber開始,以DFS(深度優(yōu)先搜索)的的順序遍歷子節(jié)點,以及生成對應(yīng)的FiberNode。
- 在遍歷過程中,為FiberNode標(biāo)記"代表不同副作用的 flags",以便后續(xù)在宿主環(huán)境中渲染的使用。
在上面我們之所以要區(qū)分fiberRootNode和HostRootFiber是因為在整個React應(yīng)用程序中開發(fā)者可以多次多次調(diào)用render方法渲染不同的組件樹,它們會有不同的HostRootFiber,但是整個應(yīng)用的根節(jié)點只有一個,那就是fiberRootNode。
執(zhí)行 ReactDOM.createRoot 會創(chuàng)建如圖所示結(jié)構(gòu):

mount 首屏渲染階段
由于是首屏渲染階段,頁面中還沒有掛載任何DOM節(jié)點,所以fiberRootNode.current指向的HostRootFiber沒有任何子Fiber節(jié)點(即current Fiber樹為空)。
當(dāng)前僅有一個HostRootFiber,對應(yīng)"首屏渲染時只有根節(jié)點的空白畫面"。
render 生成workInProgress樹階段
接下來進(jìn)入render階段,根據(jù)組件返回的JSX在內(nèi)存中依次構(gòu)建創(chuàng)建Fiber節(jié)點并連接在一起構(gòu)建Fiber樹,被稱為workInProgress Fiber樹。
在構(gòu)建workInProgress Fiber樹時會嘗試復(fù)用current Fiber樹中已有的Fiber節(jié)點內(nèi)的屬性,(在首屏渲染時,只有HostRootFiber),也可以理解為首屏渲染時,它以自己的身份生成了一個workInProgress 樹只不過還是HostRootFiber(HostRootFiber.alternate。
基于DFS(深度優(yōu)先搜索)依次生成的workInProgress節(jié)點,并連接起來構(gòu)成wip 樹的過程如圖所示:

上圖中已構(gòu)建完的workInProgress Fiber樹會在commit階段被渲染到頁面。
commit 階段
等到頁面渲染完成時,workInProgress Fiber樹會替換之前的current Fiber樹,進(jìn)而fiberRootNode的current指針會指向新的current Fiber樹。
完成雙緩存樹的切換工作,曾經(jīng)的Wip Fiber樹變?yōu)閏urrent Fiber樹。
過程如圖所示:

update 時 Fiber Tree的更迭
- 接下來我們點擊p節(jié)點觸發(fā)狀態(tài)改變。這會開啟一次新的render階段并構(gòu)建一課新的workInProgress Fiber樹。
和mount時一樣,workInProgress Fiber的創(chuàng)建可以復(fù)用current Fiber樹對應(yīng)節(jié)點的數(shù)據(jù),這個決定是否服用的過程就是Diff算法, 后面章節(jié)會詳細(xì)講解。

- workInProgress Fiber樹在render階段完成構(gòu)建后會進(jìn)入commit階段渲染到頁面上。渲染完成后,workInProgress Fiber樹變?yōu)閏urrent Fiber樹。

render 階段的流程
接下來,我們來看看用原理,在源碼中它是如何實現(xiàn)的。
Reconciler工作的階段在 React 內(nèi)部被稱為 render 階段,ClassComponent 的render函數(shù)、Function Component函數(shù)本身也都在 render 階段被調(diào)用。
根據(jù)Scheduler調(diào)度的結(jié)果不同,render階段可能開始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的調(diào)用。
也就是說React在執(zhí)行render階段的初期會依賴于Scheduler(調(diào)度器)的結(jié)果來判斷執(zhí)行哪個方法,比如Scheduler(調(diào)度器)會根據(jù)任務(wù)的優(yōu)先級選擇執(zhí)行performSyncWorkOnRoot或performConcurrentWorkOnRoot方法。這取決于任務(wù)的類型和優(yōu)先級。同步任務(wù)通常具有較高優(yōu)先級,需要立即執(zhí)行,而并發(fā)任務(wù)會在空閑時間段執(zhí)行以避免阻塞主線程。
這里補(bǔ)充一下,調(diào)度器可能的執(zhí)行結(jié)果,以用來判斷執(zhí)行什么入口函數(shù):
如果不知道調(diào)度器的執(zhí)行結(jié)構(gòu)都有哪幾類,可以跳過這段代碼向下看:
現(xiàn)在還不需要學(xué)習(xí)這兩個方法,只需要知道在這兩個方法中會調(diào)用 performUnitOfWork方法就好。
可以看到,它們唯一的區(qū)別就是是否會調(diào)用shouldYield。如果當(dāng)前瀏覽器幀沒有剩余時間,shouldYield會終止循環(huán),直到瀏覽器有空閑時間再繼續(xù)遍歷。
也就說當(dāng)更新正在進(jìn)行時,如果有 "更高優(yōu)先級的更新" 產(chǎn)生,則會終端當(dāng)前更新,優(yōu)先處理高優(yōu)先級更新。
高優(yōu)先級的更新比如:"鼠標(biāo)懸停","文本框輸入"等用戶更易感知的操作。
workInProgress代表當(dāng)前正在工作的一個fiberNode,它是一個全局的指針,指向當(dāng)前正在工作的 fiberNode,一般是workInProgress。
performUnitOfWork方法會創(chuàng)建下一個Fiber節(jié)點,并賦值給workInProgress,并將workInProgress與已經(jīng)創(chuàng)建好的Fiber節(jié)點連接起來構(gòu)成Fiber樹。
這里為什么指向的是 workInProgress 呢? 因為在每次渲染更新時,即將展示到界面上的是 workInProgress 樹,只有在首屏渲染的時候它才為空。
render階段流程概覽
Fiber Reconciler是從Stack Reconciler重構(gòu)而來,通過遞歸遍歷的方式實現(xiàn)可中斷的遞歸。 因為可以把performUnitOfWork方法分為兩部分:"遞"和"歸"。
"遞" 階段會從 HostRootFiber開始向下以 DFS 的方式遍歷,為遍歷到的每個fiberNode執(zhí)行beginWork方法。該方法會根據(jù)傳入的fiberNode創(chuàng)建下一級fiberNode。
當(dāng)遍歷到葉子元素(不包含子fiberNode)時,performUnitOfWork就會進(jìn)入 "歸" 的階段。
"歸" 階段會調(diào)用completeWork方法處理fiberNode。當(dāng)某個fiberNode執(zhí)行完complete方法后,如果其存在兄弟fiberNode(fiberNode.sibling !== null),會進(jìn)入其兄弟fiber的"遞階段"。如果不存在兄弟fiberNode,會進(jìn)入父fiberNode的 "歸" 階段。
遞階段和歸階段會交錯執(zhí)行直至HostRootFiber的"歸"階段。到此,render階段的工作就結(jié)束了。
舉一個例子:
當(dāng)執(zhí)行完深度優(yōu)先搜索之后形成的workInProgress樹。

圖中的數(shù)組是遍歷過程中的順序,可以看到,遍歷的過程中會從應(yīng)用的根節(jié)點RootFiberNode開始,依次執(zhí)行beginWork和completeWork,最后形成一顆Fiber樹,每個節(jié)點以child和return項鏈。
注意:當(dāng)遍歷到只有一個子文本節(jié)點的Fiber時,該Fiber節(jié)點的子節(jié)點不會執(zhí)行beginWork和completeWork,如圖中的"jasonshu"文本節(jié)點。這是react的一種優(yōu)化手段
剛剛提到:workInProgress代表當(dāng)前正在工作的一個fiberNode,它是一個全局的指針,指向當(dāng)前正在工作的 fiberNode,一般是workInProgress。
知道了beginWork和completeWork它們是怎樣的流程后,我們再來看它是如何實現(xiàn)的:
這段代碼主要計算FiberNode節(jié)點的變化,更新workInProgress,beginWork函數(shù)的最初運行也是在下面這個函數(shù)中,同時它也完成遞和歸兩個階段的操作。
在下面的函數(shù)中主要進(jìn)行歸的操作:
到此,Reconciler的工作架構(gòu)架子我們就搭完了。
接下來我們來講在構(gòu)建過程中每個Fiber節(jié)點具體是如何創(chuàng)建的呢?在下一篇會詳細(xì)講解beginWork和completeWork是如何實現(xiàn)的?會正式進(jìn)入render階段的實現(xiàn)了。
































