iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
JavaScript

React 觀念架構:從js 基礎到Hook 底層邏輯 系列 第 17 篇

Day 17 useEffect 生命週期與 Sync 同步觀念

  • 分享至 

  • xImage
  •  

我們解析了 useState 的底層單向鏈表與 State 的不可變性。今天我們要討論 React 中被最多次誤用、也最容易引發 Race Condition 的 Hook:useEffect。

許多人習慣用「類別組件 (Class Component) 的生命週期(componentDidMount / componentDidUpdate / componentWillUnmount)」來思考 useEffect,這其實是理解它的最大障礙。useEffect 的核心本質是「與外部系統進行同步 (Synchronization)」,而不是處理生命週期事件!

一、 心智模型轉變:從「生命週期」到「數據同步 (Sync)」

React 的渲染(Render)應該是純粹的 (Pure)——給定相同的 State 與 Props,就回傳相同的 JSX,不該包含任何副作用(Side Effects,如發送 API、註冊 Event Listener、手動操作 DOM)。

useEffect 的真正使命是:「在 React 渲染完成(Commit 階段)後,讓組件狀態與 React 以外的外部系統(如 WebSocket、Timer、DOM)保持同步。」

State 變動 ──► React 渲染 UI (Pure) ──► 畫面更新 (Commit) ──► 執行 useEffect (與外部同步)

二、 依賴陣列 (Dependency Array)

useEffect(effectFunction, deps) 的第二個參數是依賴陣列,它控制了 Effect 的執行時機:

沒給 deps:useEffect(() => {})

時機:每一次 Render 完成後都會執行。極易造成死迴圈!

給予空陣列 []:useEffect(() => {}, [])

時機:僅在 Mount(初次渲染完成) 後執行一次。

給予特定依賴 [a, b]:useEffect(() => {}, [a, b])

時機:Mount 時執行一次;後續只要 a 或 b 的記憶體參考發生改變,就會重新執行。

核心鐵則 (Linter Rule):只要在 Effect 內部有讀取到的 Props、State 或內部變數,全部都必須誠實填入依賴陣列中!隱瞞依賴會引發 Stale Closure(過時閉包)陷阱。

三、 Cleanup Function(清除函數)的執行謎團

useEffect 可以回傳一個函數(Cleanup Function),這是維護外部同步狀態不可或缺的防線:

useEffect(() => {
  // 1. Setup(設定):建立同步連接
  const socket = connectToChatRoom(roomId);

  // 2. Cleanup(清除):中斷舊連接
  return () => {
    socket.disconnect();
  };
}, [roomId]);

Cleanup 的真實執行時機:
許多人以為 Cleanup 只會在「組件 Unmount(銷毀)」時執行,這是巨大的誤解!

真實情況:當 roomId 改變觸發重新渲染時,React 會先執行「上一次 Effect 回傳的 Cleanup」(斷開舊房間),隨後才執行「全新的 Effect Setup」(連接新房間)!

[roomId 改變為 'room2'] ──► 執行 room1 的 Cleanup ──► 執行 room2 的 Effect Setup

四、 競態條件 (Race Condition) 的完美解法

在 API 請求中,若使用者快速切換頁面或搜尋條件,先發出的請求可能比後發出的請求晚回傳,導致舊資料覆蓋新資料(Race Condition)。

利用 useEffect 的 Cleanup 閉包旗標,可以完美解決此問題:

useEffect(() => {
  let isCancelled = false; // 閉包旗標

  async function fetchData() {
    const data = await apiGetArticle(articleId);
    // 如果在 API 回傳前 articleId 已改變,isCancelled 會被 Cleanup 設為 true
    if (!isCancelled) {
      setArticle(data);
    }
  }

  fetchData();

  return () => {
    isCancelled = true; // 當 articleId 變更或組件 Unmount 時,標記上一次請求無效
  };
}, [articleId]);

上一篇
Day 16 useState 底層原理與 State Immutability(不可變性)
系列文
React 觀念架構:從js 基礎到Hook 底層邏輯 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言