Skip to main content

React 畫面更新的流程機制:reconciliation

1. React 畫面更新的流程機制:Reconciliation

在 React 中,當你的應用程式狀態(state)或屬性(props)發生變化時,React 會透過一個稱為 reconciliation(協調) 的過程來決定如何高效地更新畫面。Reconciliation 是 React 的核心機制,它負責比較「舊的虛擬 DOM(Virtual DOM)」和「新的虛擬 DOM」,找出需要更新的部分,並以最小的成本更新真實的 DOM。

React 的畫面更新流程可以分為兩個主要階段:

  • Render Phase(渲染階段)

  • Commit Phase(提交階段)

接下來,我會詳細說明這兩個階段,以及 reconciliation 在其中的角色。


2. Render Phase 與 Commit Phase

React 的畫面更新流程分為兩個階段:Render PhaseCommit Phase。這兩個階段共同完成從狀態改變到畫面更新的過程。

Render Phase(渲染階段)

Render Phase 是 React 執行「渲染」的階段,負責生成新的虛擬 DOM 並進行比較。在此階段,React 會:

  1. 觸發元件的 render 函數

    • 當某個元件的 state 或 props 改變時(例如透過 setState 或父元件傳遞新的 props),React 會重新執行該元件的 render 函數,生成新的虛擬 DOM 樹。

    • 虛擬 DOM 是一個輕量級的 JavaScript 物件,代表真實 DOM 的結構。

  2. 執行 Reconciliation(協調)

    • React 會將新的虛擬 DOM 與之前的虛擬 DOM 進行比較,這個比較過程就是 reconciliation

    • Reconciliation 會找出兩棵虛擬 DOM 樹之間的差異(diffing),例如哪些元素被新增、刪除或修改。

    • React 使用高效的 diffing 演算法,只比較同層級的節點,減少計算量。

  3. 決定需要更新的部分

    • 根據比較結果,React 會標記需要更新的元件和 DOM 節點。

    • 在這個階段,React 不會直接操作真實 DOM,因此這個過程是高效且非同步的。

注意

  • Render Phase 是純計算階段,不會產生副作用(side effect),例如不會直接改變瀏覽器的畫面。

  • 如果元件使用了 React.memo 或 shouldComponentUpdate 等優化方式,React 可能會跳過某些子元件的重新渲染。

Commit Phase(提交階段)

Commit Phase 是 React 將變更應用到真實 DOM 的階段。在此階段,React 會:

  1. 更新真實 DOM

    • React 根據 Render Phase 的比較結果,將必要的變更應用到真實的 DOM 上,例如新增、移除或修改 DOM 節點。

    • 這是唯一會直接影響瀏覽器畫面的階段。

  2. 執行副作用(Side Effects)

    • 在 Commit Phase 中,React 會觸發像 useEffect 或 componentDidMount、componentDidUpdate 等生命週期方法或 Hook,這些方法通常用來執行 DOM 操作、發送網路請求等副作用。
  3. 完成更新

    • 一旦真實 DOM 更新完成,畫面就會反映最新的狀態。

注意

  • Commit Phase 通常很快,因為 React 已經在 Render Phase 中計算好最小的變更範圍。

  • Commit Phase 是同步的,確保畫面更新不會中斷。


3. Reconciliation(協調)

Reconciliation 是 React 高效更新畫面的核心機制。它的主要工作是比較新舊虛擬 DOM 樹,找出差異,並決定如何以最小的成本更新真實 DOM。以下是 Reconciliation 的運作細節:

  1. 虛擬 DOM 的作用

    • 虛擬 DOM 是 React 內部的資料結構,用來表示 UI 的狀態。

    • 每次狀態改變時,React 會生成一棵新的虛擬 DOM 樹。

  2. Diffing 演算法

    • React 使用高效的比較演算法來找出新舊虛擬 DOM 的差異。

    • 主要原則包括:

      • 同層級比較:React 只比較同一層級的節點,不會跨層級比較。

      • 不同類型節點:如果兩個節點的類型(例如 div 變成 span)不同,React 會直接重建該節點及其子樹。

      • key 屬性:在列表渲染時,React 使用 key 屬性來識別元素,減少不必要的重新渲染。

  3. 高效更新

    • 透過 diffing,React 會計算出最小的變更集合(例如只更新某個文字內容或新增一個節點)。

    • 這些變更會在 Commit Phase 中應用到真實 DOM。

程式碼範例(展示 Reconciliation 的基本流程):

假設你有一個簡單的計數器元件,當點擊按鈕時,狀態改變並觸發畫面更新:

import React, { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1); // 觸發狀態更新
};

console.log("渲染 Counter 元件");

return (
<div>
<p>目前計數:{count}</p>
<button onClick={handleClick}>1</button>
</div>
);
}

export default Counter;

運作流程:

  1. 初始渲染

    • React 執行 Counter 元件的 render 函數,生成虛擬 DOM(包含 <div><p><button>)。

    • 在 Commit Phase 中,React 將虛擬 DOM 轉換為真實 DOM,顯示在畫面上。

  2. 點擊按鈕

    • 呼叫 setCount,改變 count 的值。

    • Render Phase

      • React 重新執行 Counter 的 render 函數,生成新的虛擬 DOM。

      • Reconciliation 比較新舊虛擬 DOM,發現只有 <p> 內的文字內容改變(從 目前計數:0 變成 目前計數:1)。

    • Commit Phase

      • React 只更新 <p> 節點的文字內容,不會重新創建 <div><button>
  3. 畫面更新

    • 瀏覽器顯示新的計數值。

4. setState 觸發的 re-render 會連帶觸發子 component 的 re-render

當父元件的 state 改變時,React 會重新渲染父元件,並預設也會重新渲染所有的子元件,即使子元件的 props 沒有改變。這是因為 React 的預設行為是「當父元件重新渲染時,子元件也會跟著重新渲染」。

為什麼會這樣?

  • React 的 Reconciliation 過程會從父元件開始,逐層檢查所有子元件。

  • 如果子元件的 props 或 state 沒有改變,React 仍然會執行子元件的 render 函數,生成新的虛擬 DOM,然後比較是否需要更新真實 DOM。

  • 雖然這種重新渲染通常不會影響性能(因為虛擬 DOM 比較很快),但在大型應用程式中,過多的重新渲染可能導致性能問題。

程式碼範例(展示父子元件重新渲染):

以下是一個父元件 Parent 和子元件 Child,當父元件的狀態改變時,子元件也會重新渲染:

import React, { useState } from "react";

// 子元件
function Child({ name }) {
console.log(`渲染 Child 元件,name: ${name}`);
return <p>我是子元件,名稱:{name}</p>;
}

// 父元件
function Parent() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1); // 改變父元件狀態
};

console.log("渲染 Parent 元件");

return (
<div>
<p>父元件計數:{count}</p>
<button onClick={handleClick}>1</button>
<Child name="小明" /> {/* 子元件,props 不變 */}
</div>
);
}

export default Parent;

運作流程:

  1. 初始渲染

    • React 渲染 Parent 元件,生成虛擬 DOM。

    • 接著渲染 Child 元件,生成子元件的虛擬 DOM。

    • 最終將完整的虛擬 DOM 轉為真實 DOM,顯示在畫面上。

  2. 點擊按鈕

    • 呼叫 setCount,改變 count 的值。

    • Render Phase

      • React 重新執行 Parent 的 render 函數,生成新的虛擬 DOM。

      • 因為 Parent 包含 Child,React 也會重新執行 Child 的 render 函數,即使 Child 的 name props 沒有改變。

      • Reconciliation 比較新舊虛擬 DOM,發現只有 Parent 的 <p> 文字內容改變(例如從 父元件計數:0 變成 父元件計數:1)。

    • Commit Phase

      • React 只更新 <p> 的文字內容,Child 的真實 DOM 不會改變(因為 name 沒變)。
  3. 觀察結果

    • 在控制台中,你會看到 渲染 Parent 元件 和 渲染 Child 元件,name: 小明 都被印出,證明即使 Child 的 props 沒變,它仍然被重新渲染。

如何避免不必要的子元件重新渲染?

為了優化性能,你可以使用以下方法避免不必要的子元件重新渲染:

  1. 使用 React.memo

    • React.memo 是一個高階元件(HOC),可以讓函數式元件只在 props 改變時重新渲染。

    • 修改 Child 元件如下:

import React, { memo } from "react";

// 使用 React.memo 包裹子元件
const Child = memo(({ name }) => {
console.log(`渲染 Child 元件,name: ${name}`);
return <p>我是子元件,名稱:{name}</p>;
});

export default Child;
  • 現在,當 Parent 的 count 改變時,Child 不會重新渲染,因為它的 name props 沒有改變。
  1. 使用 useMemo 或 useCallback

    • 如果父元件傳遞的是複雜的物件或函數作為 props,可以使用 useMemo 或 useCallback 確保這些 props 的引用不變。

    • 範例:

import React, { useState, useCallback } from "react";
import { memo } from "react";

// 子元件
const Child = memo(({ name, onClick }) => {
console.log(`渲染 Child 元件,name: ${name}`);
return <p onClick={onClick}>我是子元件,名稱:{name}</p>;
});

// 父元件
function Parent() {
const [count, setCount] = useState(0);

// 使用 useCallback 確保函數引用不變
const handleChildClick = useCallback(() => {
console.log("子元件被點擊");
}, []);

const handleClick = () => {
setCount(count + 1);
};

console.log("渲染 Parent 元件");

return (
<div>
<p>父元件計數:{count}</p>
<button onClick={handleClick}>1</button>
<Child name="小明" onClick={handleChildClick} />
</div>
);
}

export default Parent;
  • 在這個例子中,useCallback 確保 handleChildClick 的引用不變,結合 React.memo,Child 只有在 name 或 onClick 改變時才會重新渲染。

總結

  1. Reconciliation 是 React 高效更新畫面的核心,透過比較新舊虛擬 DOM,找出最小的變更集合。

  2. Render Phase 負責生成新的虛擬 DOM 並執行 diffing,不會直接影響真實 DOM。

  3. Commit Phase 將變更應用到真實 DOM,並執行副作用。

  4. setState 觸發的重新渲染 會導致父元件及其子元件重新執行 render,即使子元件的 props 沒變。

  5. 使用 React.memo、useMemo 或 useCallback 可以避免不必要的子元件重新渲染,提升性能。

實作建議

  • 如果你正在開發 React 應用程式,建議在開發初期就考慮使用 React.memo 來優化子元件渲染。

  • 使用 Chrome 的 React Developer Tools,可以檢視哪些元件被重新渲染,幫助你找出性能瓶頸。

  • 當傳遞複雜的 props(如物件或函數)時,記得使用 useMemo 或 useCallback 來穩定引用。