前言
通過??上一篇??文章的學習,了解了Fiber是什么,知道了Fiber節點可以保存對應的DOM節點。Fiber節點構成的Fiber Tree會對應DOM Tree。
前面也提到Fiber是一種新的調和算法,那么它是如何更新DOM節點的呢?
單個節點的創建更新流程
對于同一個節點,React 會比較這個節點的ReactElement與FiberNode,生成子FiberNode。并根據比較的結果生成不同標記(插入、刪除、移動...),對應不同宿主環境API的執行。
根據上面的Reconciler的工作流程,舉一個例子:
比如:
mount階段,掛載<div></div>。
- 先通過jsx("div")生成 React Element <div></div>。
- 生成的對應的fiberNode為null(由于是由于是掛載階段,React還未構建組件樹)。
- 生成子fiberNode(實際上就是這個div的fiber節點)。
- 生成Placement標記。
將<div></div>更新為<p></p>。
update階段,更新將<div></div>更新為<p></p>。
- 先通過jsx("p")生成 React Element <p></p>。
- p與對應的fiberNode作比較(FiberNode {type: 'div'})。
- 生成子fiberNode為null。
- 生成對應標記Delement Placement。
用一張圖解釋上面的流程:
當所有的ReactElement比較完后,會生成一顆fiberNode Tree,一共會存在兩棵fiberNode Tree。
- current:與視圖中真實UI對應的fiberNode樹。
- workInProgress:觸發更新后,正在reconciler中計算的fiberNode Tree(用于下一次的視圖更新,在下一次視圖更新后,會變成current Tree)。
這就是React中的"雙緩存樹"技術。
什么是"雙緩存"?
雙緩存技術是一種計算機圖形學中用于減少屏幕閃爍和提高渲染性能的技術。
就好像你是一個畫家,你需要在一個畫布上繪制一幅畫。在沒有雙緩存技術的情況下,你會直接在畫布上作畫。當你繪制一條線或一個形狀時,觀眾會立即看到這個過程。如果你的繪畫速度較慢,觀眾可能會看到畫面的閃爍和變化,這會導致視覺上的不舒適。
引入雙緩存技術就好比你有兩個畫布:一個是主畫布,觀眾可以看到它;另一個是隱藏畫布,觀眾看不到它。在這種情況下,你會在隱藏畫布上進行繪畫。當你完成一個階段性的繪制任務后,你將隱藏畫布上的圖像瞬間復制到主畫布上。觀眾只能看到主畫布上的圖像,而看不到隱藏畫布上的繪制過程。這樣,即使你的繪畫速度較慢,觀眾也不會看到畫面的閃爍和變化,從而獲得更流暢的視覺體驗。
使用雙緩存技術時,計算機會在一個隱藏的緩沖區(后臺緩沖區)上進行繪制,然后將繪制好的圖像一次性復制到屏幕上(前臺緩沖區)。這樣可以減少屏幕閃爍,并提高渲染性能。
這種在內存中構建并直接替換的技術叫作雙緩存。
React 中使用"雙緩存"來完成Fiber Tree的構建與替換,對應著DOM Tree的創建于與更新。
雙緩存Fiber樹
Fiber架構中同時存在兩棵Fiber Tree,一顆是"真實UI對應的 Fiber Tree"可以理解為前緩沖區。另一課是"正在內存中構建的 Fiber Tree"可以理解為后緩沖區,這里值宿主環境(比如瀏覽器)。
當前屏幕上顯示內容對應的Fiber樹稱為current Fiber樹,正在內存中構建的Fiber樹稱為workInProgress Fiber樹。
current Fiber樹中的Fiber節點被稱為current fiber,workInProgress Fiber樹中的Fiber節點被稱為workInProgress fiber,他們通過alternate屬性連接。
雙緩存樹一個顯著的特點就是兩棵樹之間會互相切換,通過alternate屬性連接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
雙緩存樹切換的規則
React應用的根節點通過current指針在不同Fiber樹的HostRootFiber根節點(ReactDOM.render創建的根節點)間切換。
- 在 mount時(首次渲染),會根據jsx方法返回的React Element構建Fiber對象,形成Fiber樹。
- 然后這棵Fiber樹會作為current Fiber應用到真實DOM上。
- 在 update時(狀態更新),會根據狀態變更后的React Element和current Fiber作對比形成新的workInProgress Fiber樹。
- 即當workInProgress Fiber樹構建完成交給Renderer(渲染器)渲染在頁面上后,應用根節點的current指針指向workInProgress Fiber樹。
- 然后workInProgress Fiber切換成current Fiber應用到真實DOM上,這就達到了更新的目的。
這一切都是在內存中發生的,從而減少了對DOM的直接操作。
每次狀態更新都會產生新的workInProgress Fiber樹,通過current與workInProgress的替換,完成DOM更新,這就是React中用的雙緩存樹切換規則。
Renderer 是一個與特定宿主環境(如瀏覽器 DOM、服務器端渲染、React Native 等)相關的模塊。Renderer 負責將 React 組件樹轉換為特定宿主環境下的實際 UI。從而使 React 能夠在多個平臺上運行。
上面的語言可能有些枯燥,我們來畫個圖演示一下。
比如有下面這樣一段代碼,點擊元素把div切換成p元素:
function App() {
const [elementType, setElementType] = useState('div');
const handleClick = () => {
setElementType(prevElementType => {
return prevElementType === 'div' ? 'p' : 'div';
})
}
// 根據 elementType 的值動態創建對應的元素
const Element = elementType;
return (
<div>
<Element onClick={handleClick}>
點擊我切換 div 和 p 標簽
</Element>
</div>
)
}
const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />);
接下來,我們分別從 mount(首次渲染)和 update(更新)兩個角度講解 Fiber 架構的工作原理。
mount 時 Fiber Tree的構建
mount 時有兩種情況:
- 整個應用的首次渲染,這種情況發生首次進入頁面時。
- 某個組件的首次渲染,當 isShow 為 true時,Btn 組件進入 mount 首次渲染流程。
{isShow ? <Btn /> : null}
假如有這樣一段代碼:
function App() {
const [num, add] = useState(0);
return (
<p onClick={() => add(num + 1)}>{num}</p>
)
}
const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />)
mount 時上面的Fiber樹構建過程如下:
- 首次執行ReactDOM.createRoot(root)會創建fiberRootNode。
- 接著執行到render(<App />)時會創建HostRootFiber,實際上它是一個HostRoot節點。
fiberRootNode 是整個應用的根節點,HostRootFiber 是 <App /> 所在組件樹的根節點。
- 從HostRootFiber開始,以DFS(深度優先搜索)的的順序遍歷子節點,以及生成對應的FiberNode。
- 在遍歷過程中,為FiberNode標記"代表不同副作用的 flags",以便后續在宿主環境中渲染的使用。
在上面我們之所以要區分fiberRootNode和HostRootFiber是因為在整個React應用程序中開發者可以多次多次調用render方法渲染不同的組件樹,它們會有不同的HostRootFiber,但是整個應用的根節點只有一個,那就是fiberRootNode。
執行 ReactDOM.createRoot 會創建如圖所示結構:
mount 首屏渲染階段
由于是首屏渲染階段,頁面中還沒有掛載任何DOM節點,所以fiberRootNode.current指向的HostRootFiber沒有任何子Fiber節點(即current Fiber樹為空)。
當前僅有一個HostRootFiber,對應"首屏渲染時只有根節點的空白畫面"。
<body>
<div id="root"></div>
</body>
render 生成workInProgress樹階段
接下來進入render階段,根據組件返回的JSX在內存中依次構建創建Fiber節點并連接在一起構建Fiber樹,被稱為workInProgress Fiber樹。
在構建workInProgress Fiber樹時會嘗試復用current Fiber樹中已有的Fiber節點內的屬性,(在首屏渲染時,只有HostRootFiber),也可以理解為首屏渲染時,它以自己的身份生成了一個workInProgress 樹只不過還是HostRootFiber(HostRootFiber.alternate。
基于DFS(深度優先搜索)依次生成的workInProgress節點,并連接起來構成wip 樹的過程如圖所示:
上圖中已構建完的workInProgress Fiber樹會在commit階段被渲染到頁面。
commit 階段
等到頁面渲染完成時,workInProgress Fiber樹會替換之前的current Fiber樹,進而fiberRootNode的current指針會指向新的current Fiber樹。
完成雙緩存樹的切換工作,曾經的Wip Fiber樹變為current Fiber樹。
過程如圖所示:
update 時 Fiber Tree的更迭
- 接下來我們點擊p節點觸發狀態改變。這會開啟一次新的render階段并構建一課新的workInProgress Fiber樹。
和mount時一樣,workInProgress Fiber的創建可以復用current Fiber樹對應節點的數據,這個決定是否服用的過程就是Diff算法, 后面章節會詳細講解。
- workInProgress Fiber樹在render階段完成構建后會進入commit階段渲染到頁面上。渲染完成后,workInProgress Fiber樹變為current Fiber樹。
render 階段的流程
接下來,我們來看看用原理,在源碼中它是如何實現的。
Reconciler工作的階段在 React 內部被稱為 render 階段,ClassComponent 的render函數、Function Component函數本身也都在 render 階段被調用。
根據Scheduler調度的結果不同,render階段可能開始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的調用。
也就是說React在執行render階段的初期會依賴于Scheduler(調度器)的結果來判斷執行哪個方法,比如Scheduler(調度器)會根據任務的優先級選擇執行performSyncWorkOnRoot或performConcurrentWorkOnRoot方法。這取決于任務的類型和優先級。同步任務通常具有較高優先級,需要立即執行,而并發任務會在空閑時間段執行以避免阻塞主線程。
這里補充一下,調度器可能的執行結果,以用來判斷執行什么入口函數:
如果不知道調度器的執行結構都有哪幾類,可以跳過這段代碼向下看:
現在還不需要學習這兩個方法,只需要知道在這兩個方法中會調用 performUnitOfWork方法就好。
// performSyncWorkOnRoot會調用該方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot會調用該方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
可以看到,它們唯一的區別就是是否會調用shouldYield。如果當前瀏覽器幀沒有剩余時間,shouldYield會終止循環,直到瀏覽器有空閑時間再繼續遍歷。
也就說當更新正在進行時,如果有 "更高優先級的更新" 產生,則會終端當前更新,優先處理高優先級更新。
高優先級的更新比如:"鼠標懸停","文本框輸入"等用戶更易感知的操作。
workInProgress代表當前正在工作的一個fiberNode,它是一個全局的指針,指向當前正在工作的 fiberNode,一般是workInProgress。
performUnitOfWork方法會創建下一個Fiber節點,并賦值給workInProgress,并將workInProgress與已經創建好的Fiber節點連接起來構成Fiber樹。
這里為什么指向的是 workInProgress 呢? 因為在每次渲染更新時,即將展示到界面上的是 workInProgress 樹,只有在首屏渲染的時候它才為空。
render階段流程概覽
Fiber Reconciler是從Stack Reconciler重構而來,通過遞歸遍歷的方式實現可中斷的遞歸。 因為可以把performUnitOfWork方法分為兩部分:"遞"和"歸"。
"遞" 階段會從 HostRootFiber開始向下以 DFS 的方式遍歷,為遍歷到的每個fiberNode執行beginWork方法。該方法會根據傳入的fiberNode創建下一級fiberNode。
當遍歷到葉子元素(不包含子fiberNode)時,performUnitOfWork就會進入 "歸" 的階段。
"歸" 階段會調用completeWork方法處理fiberNode。當某個fiberNode執行完complete方法后,如果其存在兄弟fiberNode(fiberNode.sibling !== null),會進入其兄弟fiber的"遞階段"。如果不存在兄弟fiberNode,會進入父fiberNode的 "歸" 階段。
遞階段和歸階段會交錯執行直至HostRootFiber的"歸"階段。到此,render階段的工作就結束了。
舉一個例子:
function App() {
return (
<div>
<p>1229</p>
jasonshu
</div>
)
}
const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />);
當執行完深度優先搜索之后形成的workInProgress樹。
圖中的數組是遍歷過程中的順序,可以看到,遍歷的過程中會從應用的根節點RootFiberNode開始,依次執行beginWork和completeWork,最后形成一顆Fiber樹,每個節點以child和return項鏈。
注意:當遍歷到只有一個子文本節點的Fiber時,該Fiber節點的子節點不會執行beginWork和completeWork,如圖中的"jasonshu"文本節點。這是react的一種優化手段
剛剛提到:workInProgress代表當前正在工作的一個fiberNode,它是一個全局的指針,指向當前正在工作的 fiberNode,一般是workInProgress。
// 該函數用于調度和執行 FiberNode 樹的更新和渲染過程
// 該函數的作用是處理 React 程序中更新請求,計算 FiberNode 樹中的每個節點的變化,并把這些變化同步到瀏覽器的DOM中
function workLoop() {
while (workInProgress !== null) {
// 開始執行每個工作單元的工作
performUmitOfWork(workInProgress);
}
}
知道了beginWork和completeWork它們是怎樣的流程后,我們再來看它是如何實現的:
這段代碼主要計算FiberNode節點的變化,更新workInProgress,beginWork函數的最初運行也是在下面這個函數中,同時它也完成遞和歸兩個階段的操作。
// 在這個函數中,React 會計算 FiberNode 節點的變化,并更新 workInProgress
function performUmitOfWork(fiber: FiberNode) {
// 如果有子節點,就一直遍歷子節點
const next = beginWork(fiber);
// 遞執行完之后,需要更新下工作單元的props
fiber.memoizedProps = fiber.pendingProps;
// 沒有子節點的 FiberNode 了,代表遞歸到最深層了。
if (next === null) {
completeUnitOfWork(fiber);
} else {
// 如果有子節點的 FiberNode,則更新子節點為新的 fiberNode 繼續執行
workInProgress = next;
}
}
在下面的函數中主要進行歸的操作:
// 主要進行歸的過程,向上遍歷父節點以及兄弟,更新它們節點的變化,并更新 workInProgress
function completeUnitOfWork(fiber: FiberNode) {
let node: FiberNode | null = fiber;
do {
// 歸:沒有子節點之后開始向上遍歷父節點
completeWork(node);
const sibling = node.sibling;
if (sibling !== null) {
// 有兄弟節點時,將指針指到兄弟節點
workInProgress = sibling;
return;
}
// 兄弟節點不存在時,遞歸應該繼續往上指到父親節點
node = node.return;
workInProgress = node;
} while (node !== null);
}
到此,Reconciler的工作架構架子我們就搭完了。
接下來我們來講在構建過程中每個Fiber節點具體是如何創建的呢?在下一篇會詳細講解beginWork和completeWork是如何實現的?會正式進入render階段的實現了。