React 函數組件是如何觸發更新的

React 函數組件自從 Hooks 的引入以來,成為現代 React 應用開發的核心。相比類組件,函數組件不僅更簡潔,還擁有更強大的功能。不過,理解它背後的更新機制,尤其是從源碼層面的視角,能幫助我們更好地優化性能並避免不必要的重新渲染。本文將從 React 的底層源碼出發,深度解析函數組件的更新機制。

1. 函數組件更新的觸發條件

函數組件的更新主要通過以下幾種方式觸發:

  • State 變化:通過 useState 的 setter 方法更新狀態。
  • Props 變化:當父組件傳遞的 props 發生變化時,React 會重新渲染該組件。
  • Context 變化:使用 useContext 獲取的上下文數據發生變化時,React 會重新渲染相關組件。

從源碼的角度看,React 會將組件的狀態、props 等數據存儲在內部的一個 Fiber 樹 中,當這些數據發生變化時,React 會進入調和(Reconciliation)階段,決定是否需要重新渲染組件。

2. Fiber 樹的核心作用

在 React 16 引入 Fiber 架構之後,所有組件(包括類組件和函數組件)的更新都被表示為一個 Fiber 節點。Fiber 樹的核心作用是將更新過程分為多個小任務來執行,而不是一次性完成,這樣 React 可以在渲染大任務時保持對用戶界面的響應性。

每個函數組件都會擁有一個對應的 Fiber 對象,該對象記錄了組件的 狀態(state)props 以及 更新隊列(update queue)。當調用 setStateuseState 的 setter 函數時,React 會將這個更新存入 Fiber 節點的更新隊列中,等待調度器來執行。

3. React 調度更新的過程

當函數組件的狀態或 props 發生變化時,React 會進入更新調度過程。核心流程如下:

3.1 setState 及 useState 的更新機制

useState 中的 setter 方法(如 setState)被調用時,實際上會創建一個 更新對象,該對象包含了新的狀態值以及需要更新的組件引用。這個更新對象會被加入當前 Fiber 節點的更新隊列中,等待 React 調度。

1
// 簡化後的 useState 實現
2
function useState(initialState) {
3
const hook = getHook(); // 從當前 Fiber 節點獲取 hook 狀態
4
if (!hook) {
5
// 初始化 hook 狀態
6
return mountState(initialState);
7
}
8
return updateState(hook);
9
}

每次更新,React 會根據 Fiber 樹中的每個 Fiber 節點執行更新邏輯。它通過 beginWork 函數檢查更新隊列,重新計算狀態值,並觸發組件的重新渲染。

3.2 Hooks 的存儲與重用

在函數組件的每次執行過程中,React 會通過一個內部鏈表來保存和重用 Hooks。每個 useStateuseEffect 調用都會在這條鏈表上創建或複用一個 hook 節點,從而存儲狀態值或副作用。

1
function renderWithHooks(currentFiber, nextChildren) {
2
currentlyRenderingFiber = currentFiber;
3
currentHook = currentFiber.memoizedState; // 取出之前存儲的 hook 鏈表
4
nextChildren = Component(props); // 重新執行函數組件
5
return nextChildren;
6
}

每次函數組件更新時,React 會從頭開始執行函數體,但每個 Hook 都是按照順序保存的,因此可以依次取出對應的狀態和副作用,保持一致性。

4. 調和算法與虛擬 DOM 的工作原理

4.1 虛擬 DOM 的 Diff 算法

React 的更新機制依賴於 調和算法(Reconciliation) 來決定哪些部分需要更新。調和的核心步驟包括:

  1. 創建新的虛擬 DOM:每次組件更新時,React 會根據新的狀態和 props 生成一棵新的虛擬 DOM 樹。
  2. Diffing 階段:React 比較新舊兩棵虛擬 DOM 樹,通過 Diff 算法找出需要修改的部分。
  3. 更新實際 DOM:React 將差異最小化後,批量更新實際的 DOM。

Fiber 樹使 React 可以在工作單元(更新步驟)之間暫停和恢復,優化了長任務的執行過程,提高了應用的響應性。

4.2 更新優先級與調度

React 使用 優先級隊列 來控制更新的調度順序。每次狀態或 props 變化時,React 會將更新任務賦予一個優先級,根據任務的緊急程度決定何時執行。

  • 高優先級更新(如用戶輸入事件)會立即執行。
  • 低優先級更新(如動畫或後台數據加載)則會延後執行,保證 UI 的流暢性。

5. 函數組件性能優化:useMemo 與 useCallback

由於每次組件更新都會重新執行整個函數,因此使用 useMemouseCallback 來緩存一些昂貴的計算或函數引用非常關鍵。

  • useMemo:用於緩存計算結果,避免在每次渲染時重複計算。
  • useCallback:用於緩存函數,避免在子組件中每次都創建新的函數引用。
1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
2
3
const memoizedCallback = useCallback(() => {
4
handleClick(a);
5
}, [a]);

這些 Hooks 通過緩存依賴不變的值或函數,減少了不必要的重新計算,從而提升了應用性能。

6. 函數組件的生命週期模擬

函數組件沒有類組件的生命週期方法,但通過 useEffect,我們可以實現類似的生命週期功能:

  • componentDidMountcomponentDidUpdate:使用 useEffect 模擬,它在組件掛載或更新時執行。
  • componentWillUnmount:通過在 useEffect 中返回清理函數來模擬。
1
useEffect(() => {
2
// 掛載或更新時執行
3
return () => {
4
// 卸載時執行
5
};
6
}, [dependencies]); // 依賴數組控制執行時機

總結

React 函數組件的更新過程從狀態變化開始,通過 Fiber 樹和調和算法逐步完成對虛擬 DOM 的更新,再反映到實際 DOM 中。Hooks 的引入使函數組件更加簡潔,但也需要開發者更加關注性能優化,如合理使用 useMemouseCallbackReact.memo

理解這些底層機制能幫助我們編寫更加高效的 React 應用,避免不必要的性能瓶頸。