Skip to main content

[Medium] React useEffect 與 Virtual DOM

1. What is useEffect?

什麼是 useEffect

核心概念

useEffect 是 React 函式元件中負責管理副作用(side effects)的 Hook。它會在元件渲染之後執行非同步資料請求、訂閱、DOM 操作或手動同步狀態,對應 class 元件的 componentDidMountcomponentDidUpdatecomponentWillUnmount 等生命週期方法。

常見用途

  • 取得遠端資料並更新元件狀態
  • 維護訂閱或事件監聽(如 resizescroll
  • 與瀏覽器 API 互動(如更新 document.title、操作 localStorage
  • 清除前一次渲染遺留的資源(如取消請求、移除監聽器)
點此展開基本使用範例
import { useEffect, useState } from 'react';

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

useEffect(() => {
document.title = `點擊次數:${count}`;
});

return (
<button type="button" onClick={() => setCount((prev) => prev + 1)}>
點我
</button>
);
}

2. When does useEffect run?

useEffect 何時會執行?

useEffect 的第二個參數是相依陣列(dependency array),用來控制副作用的執行時機。React 會逐一比較陣列中的每個值,在偵測到變化時重新執行副作用,並於下一輪執行前觸發清除函式。

2.1 常見依賴模式

// 1. 每次渲染後執行(含第一次)
useEffect(() => {
console.log('任意 state 改變都會觸發');
});

// 2. 僅在初次渲染時執行一次
useEffect(() => {
console.log('只會在 component mount 時執行');
}, []);

// 3. 指定相依變數
useEffect(() => {
console.log('僅在 selectedId 改變時觸發');
}, [selectedId]);

2.2 清除函式與資源回收

useEffect(() => {
const handler = () => {
console.log('監聽中');
};

window.addEventListener('resize', handler);

return () => {
window.removeEventListener('resize', handler);
console.log('解除監聽');
};
}, []);

上述範例運用清除函式解除事件監聽。React 會在元件卸載或依賴變數更新之前先執行清除函式,確保不留下記憶體洩漏與重複監聽。

3. What is the difference between Real DOM and Virtual DOM?

真實 DOM 與虛擬 DOM 的差異是什麼?

比較面向Real DOM(真實 DOM)Virtual DOM(虛擬 DOM)
結構由瀏覽器維護的實體節點由 JavaScript 物件描述的節點
更新成本直接操作會觸發排版與重繪,成本高先計算差異再批次套用,成本低
更新策略立即反映至畫面先在記憶體建立新樹再比較差異
擴充性需手動控制更新流程可以插入中介邏輯(Diff、批次)

React 為何採用虛擬 DOM

// 簡化流程示意(非實際 React 原始碼)
function renderWithVirtualDOM(newVNode, container) {
const prevVNode = container.__vnode;
const patches = diff(prevVNode, newVNode);
applyPatches(container, patches);
container.__vnode = newVNode;
}

虛擬 DOM 讓 React 可以先在記憶體中進行 Diff,取得最小化的更新清單,再一次同步至真實 DOM,避免頻繁的重排與重繪。

4. How to coordinate useEffect and Virtual DOM?

useEffect 與虛擬 DOM 如何協作?

React 的渲染流程分成 Render Phase 與 Commit Phase。useEffect 與虛擬 DOM 的配合重點在於:副作用必須等待真實 DOM 更新完成後才能執行。

Render Phase(渲染階段)

  • React 建立新的虛擬 DOM,並計算上一版虛擬 DOM 的差異
  • 此階段是純函式運算,可被中斷或重新執行

Commit Phase(提交階段)

  • React 將差異套用到真實 DOM
  • useLayoutEffect 會在此階段同步執行,以確保 DOM 已更新

Effect Execution(副作用執行時機)

  • useEffect 會在 Commit Phase 結束、瀏覽器繪製完成後執行
  • 這樣可以避免副作用阻塞畫面更新,提升使用者體驗
useEffect(() => {
const controller = new AbortController();

fetch('/api/profile', { signal: controller.signal })
.then((res) => res.json())
.then(setProfile)
.catch((error) => {
if (error.name !== 'AbortError') {
console.error('載入失敗', error);
}
});

return () => {
controller.abort(); // 確保在相依變數更新或元件卸載時取消請求
};
}, [userId]);

5. Quiz Time

小測驗時間 模擬面試情境

題目:請說明下列程式碼的執行順序並寫出輸出結果

import { useEffect, useState } from 'react';

function Demo() {
const [visible, setVisible] = useState(false);

useEffect(() => {
console.log('effect 1');
return () => {
console.log('cleanup 1');
};
});

useEffect(() => {
console.log('effect 2');
}, [visible]);

return (
<>
<p>狀態:{visible ? '顯示' : '隱藏'}</p>
<button type="button" onClick={() => setVisible((prev) => !prev)}>
切換
</button>
</>
);
}
點擊查看答案
  • 初次渲染後依序輸出 effect 1effect 2。第一個 useEffect 沒有相依陣列,第二個 useEffect 依賴 visible,初始值為 false 仍會執行一次。
  • 切換按鈕後觸發 setVisible,下一輪渲染會先執行上一輪的清除函式,輸出 cleanup 1,再執行新的 effect 1effect 2
  • 由於 visible 每次切換都會改變,effect 2 會在每次切換後重新執行。

最終輸出順序為:effect 1effect 2 → (點擊後)cleanup 1effect 1effect 2

6. Best Practices

最佳實務

推薦做法

  • 審慎維護依賴陣列,搭配 ESLint 規則 react-hooks/exhaustive-deps
  • 依據職責拆分多個 useEffect,減少大型副作用造成的耦合。
  • 在清除函式中釋放監聽器或取消非同步請求,避免記憶體洩漏。
  • 需要在 DOM 更新後立即讀取布局資訊時改用 useLayoutEffect,但要評估效能影響。

範例:拆分不同職責

useEffect(() => {
document.title = `目前使用者:${user.name}`;
}, [user.name]); // 管理 document.title

useEffect(() => {
const subscription = chatClient.subscribe(roomId);
return () => subscription.unsubscribe();
}, [roomId]); // 管理聊天室連線

7. Interview Summary

面試總結

快速複習

  1. useEffect 透過依賴陣列控制執行時機,清除函式負責資源回收。
  2. 虛擬 DOM 透過 Diff 演算法找出最小更新集,減少真實 DOM 操作成本。
  3. 理解 Render Phase 與 Commit Phase,可以精準回答副作用與渲染流程的關聯。
  4. 面試延伸可補充效能提升策略,例如批次更新、懶載入與 memoization。

面試回答模板

"React 在渲染時會先建立虛擬 DOM,計算差異後才進入 Commit Phase 更新真實 DOM。useEffect 會在提交完成、瀏覽器繪製後執行,因此適合處理非同步請求或事件監聽。只要維護正確的依賴陣列並記得清除函式,就能避免記憶體洩漏與競態問題。"

Reference

參考資料