Skip to main content

什麼是 React 的調度器(Scheduler)?

React 的調度器(Scheduler)是 React 內部的一個模組,負責管理和安排任務的執行順序與優先級。簡單來說,它就像一個「任務管理員」,決定哪些 React 更新任務(例如狀態改變、渲染更新)應該在什麼時候執行,以確保應用程式保持流暢且高效能。

React 調度器是 React 16 引入的**併發渲染(Concurrent Rendering)**功能的核心部分。它與 React 的 Fiber 架構密切相關,目的是讓 React 能夠更好地處理大量的狀態更新、複雜的元件樹,以及在不同優先級的任務之間進行協調。

為什麼需要調度器?

在早期的 React(例如 React 15 或之前),當你更新狀態(例如透過 setState),React 會立即同步地重新渲染整個元件樹。這可能導致以下問題:

  1. 阻塞主執行緒:如果元件樹很大,渲染過程會佔用主執行緒,導致頁面卡頓,使用者互動(例如點擊、輸入)變得遲緩。

  2. 無法優先處理重要任務:所有更新任務的優先級相同,無法區分哪些更新更緊急(例如使用者輸入)或哪些可以延後(例如背景資料的更新)。

React 的調度器解決了這些問題,透過以下方式:

  • 任務分割:將大任務拆分成小塊,分散執行,避免長時間阻塞主執行緒。

  • 優先級調度:根據任務的緊急程度,決定哪些任務優先執行,哪些可以延後。

  • 可中斷渲染:允許 React 在執行渲染時暫停,處理更高優先級的任務(例如使用者輸入),然後再繼續。


調度器的核心概念

為了讓你更容易理解,以下是 React 調度器的幾個核心概念,並以簡單的方式說明:

1. 任務(Task)

React 將所有的更新(例如 setState、重新渲染)視為一個個「任務」。每個任務都有自己的優先級,例如:

  • 高優先級:使用者互動(點擊、輸入)觸發的更新。

  • 低優先級:背景任務(例如從 API 獲取資料後的更新)。

調度器的職責是決定這些任務的執行順序。

2. 時間分片(Time Slicing)

React 調度器會將一個大的渲染任務拆分成多個小的「時間分片」(Time Slice)。每個分片只執行一小部分工作,然後釋放主執行緒,讓瀏覽器有機會處理其他任務(例如繪製畫面或響應使用者輸入)。

例如:

  • 假設渲染一個大型元件樹需要 50ms,調度器可能將其拆分成 10 個 5ms 的小任務。

  • 每個小任務執行後,調度器會檢查是否有更高優先級的任務需要處理。

3. 優先級(Priority)

React 調度器使用不同的優先級來管理任務,常見的優先級包括:

  • 即時(Immediate):必須立即執行的任務,例如同步更新。

  • 使用者互動(User-blocking):與使用者互動相關的更新,例如點擊按鈕後的狀態改變。

  • 普通(Normal):一般的更新任務,例如從伺服器獲取資料後的更新。

  • 低優先級(Low):可以延後的任務,例如背景資料的預處理。

  • 閒置(Idle):在瀏覽器空閒時才執行的任務。

這些優先級由 React 內部的 react-reconciler 模組和調度器共同管理。

4. 併發模式(Concurrent Mode)

React 的併發模式依賴調度器,允許 React 在執行渲染時:

  • 暫停:如果有更高優先級的任務,暫停當前渲染。

  • 繼續:在適當的時機恢復被暫停的任務。

  • 丟棄:如果某個任務不再需要(例如狀態已改變),丟棄舊的渲染結果。

這使得 React 應用程式能夠保持流暢,即使在高負載的情況下也能快速響應使用者輸入。


調度器如何運作?

React 的調度器主要由 @react-scheduler 模組實現(這是 React 內部的獨立模組)。以下是調度器運作的簡化流程:

  1. 任務創建

    • 當你呼叫 setState 或其他觸發更新的操作時,React 會創建一個更新任務。

    • 這個任務會被分配一個優先級(例如使用者互動的高優先級)。

  2. 任務排程

    • 調度器將任務加入一個內部的任務佇列(Queue)。

    • 根據優先級,調度器決定哪些任務需要立即執行,哪些可以延後。

  3. 時間分片執行

    • 調度器使用瀏覽器的 requestIdleCallback 或 requestAnimationFrame(在不支援的情況下會使用 setTimeout)來安排任務。

    • 每個時間分片執行一小部分工作,然後檢查是否需要暫停。

  4. 檢查優先級

    • 如果有更高優先級的任務(例如使用者點擊),調度器會暫停當前任務,優先執行新的任務。

    • 完成高優先級任務後,恢復被暫停的任務。

  5. 完成渲染

    • 一旦所有任務完成,React 會將最終的更新提交到 DOM,完成畫面渲染。

程式碼範例:體驗調度器的效果

雖然 React 的調度器是內部模組,我們無法直接操作它,但可以透過 React 的併發模式功能(例如 useTransition)來間接體驗調度器的作用。以下是一個簡單的範例,展示如何使用 useTransition 來處理高優先級和低優先級的更新。

範例:搜尋清單的非阻塞渲染

假設你正在開發一個搜尋清單的應用程式,當使用者輸入搜尋關鍵字時,清單會即時更新,但清單資料量很大,渲染可能會導致輸入框卡頓。我們可以使用 useTransition 來讓輸入框保持流暢。

以下是完整的程式碼:

import React, { useState, useTransition } from "react";

// 模擬一個大型清單的資料
const largeList = Array.from({ length: 10000 }, (_, index) => ({
id: index,
name: `項目 ${index + 1}`,
}));

function SearchList() {
const [query, setQuery] = useState("");
const [filteredList, setFilteredList] = useState(largeList);
const [isPending, startTransition] = useTransition();

// 處理輸入框的變化
const handleInputChange = (event) => {
const value = event.target.value;
setQuery(value);

// 使用 useTransition 將清單過濾設為低優先級任務
startTransition(() => {
const filtered = largeList.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredList(filtered);
});
};

return (
<div>
<h2>搜尋清單</h2>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="輸入關鍵字搜尋..."
/>
{isPending && <p>正在過濾清單...</p>}
<ul>
{filteredList.slice(0, 100).map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

export default SearchList;

程式碼逐步說明

  1. 匯入 useTransition

    • useTransition 是 React 提供的一個 Hook,允許你將某些更新標記為「低優先級」,交由調度器管理。

    • isPending 表示低優先級任務是否正在執行,startTransition 用來包裝低優先級的更新。

  2. 模擬大型資料

    • largeList 是一個包含 10,000 筆資料的陣列,模擬一個大型清單的過濾場景。
  3. 處理輸入事件

    • 當使用者輸入時,setQuery 會立即更新輸入框的狀態(高優先級,保持輸入流暢)。

    • startTransition 將清單過濾的邏輯標記為低優先級任務,交給調度器處理。

  4. 顯示過濾結果

    • 使用 filteredList 渲染過濾後的清單,並限制只顯示前 100 筆資料,避免渲染過多 DOM 元素。

    • isPending 用來顯示「正在過濾」的提示,提醒使用者背景任務正在進行。

執行結果

  • 輸入框流暢:即使清單過濾需要較長時間(因為資料量大),輸入框的響應仍然很快,因為 setQuery 是高優先級任務。

  • 清單更新稍慢:清單的過濾和渲染被標記為低優先級,調度器會在輸入框更新完成後,利用空閒時間分片執行過濾邏輯。

  • 使用者體驗提升:使用者不會因為清單過濾而感到輸入卡頓,應用程式整體感覺更流暢。


如何在專案中使用調度器?

雖然 React 的調度器是內部模組,但你可以在專案中透過以下方式間接利用它的優勢:

  1. 啟用併發模式

    • 使用 ReactDOM.createRoot(React 18 開始)來啟用併發渲染:

      import React from "react";
      import ReactDOM from "react-dom/client";
      import App from "./App";

      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(<App />);
    • 這會啟用 React 的併發功能,讓調度器自動管理任務。

  2. 使用 useTransition

    • 如上述範例所示,使用 useTransition 將非緊急的更新標記為低優先級。
  3. 使用 useDeferredValue

    • 如果你有某個值(例如搜尋關鍵字)需要延遲更新,可以使用 useDeferredValue:

      import React, { useState, useDeferredValue } from "react";

      function DeferredSearch() {
      const [query, setQuery] = useState("");
      const deferredQuery = useDeferredValue(query);

      const filteredList = largeList.filter((item) =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
      );

      return (
      <div>
      <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="輸入關鍵字搜尋..."
      />
      <ul>
      {filteredList.slice(0, 100).map((item) => (
      <li key={item.id}>{item.name}</li>
      ))}
      </ul>
      </div>
      );
      }

      export default DeferredSearch;
    • useDeferredValue 會延遲 deferredQuery 的更新,讓輸入框保持流暢。

  4. 避免過多同步更新

    • 避免在事件處理函數中直接執行大量計算,改用 useTransition 或 useDeferredValue 將計算移到背景。

常見問題

1. 調度器會影響我的程式碼撰寫方式嗎?

一般情況下,你不需要直接操作調度器。React 的併發功能(例如 useTransition、useDeferredValue)已經封裝了調度器的功能,你只需要使用這些 API 即可。如果你在 React 18 中使用 createRoot,調度器會自動生效。

2. 什麼時候應該使用 useTransition?

當你有以下場景時,可以考慮使用 useTransition:

  • 需要處理大量資料的更新(例如過濾大型清單)。

  • 希望優先保證某些 UI 的響應速度(例如輸入框或按鈕)。

  • 想讓背景任務不影響使用者體驗。

3. 調度器會讓應用程式變慢嗎?

不會!調度器的目的是讓應用程式更流暢。它透過分片執行和優先級管理,避免主執行緒被長時間阻塞。雖然某些低優先級任務可能延後執行,但整體使用者體驗會更好。


總結

React 的調度器(Scheduler)是 React 併發渲染的關鍵模組,負責管理任務的優先級和執行時機。它讓 React 應用程式能夠在處理複雜更新時保持流暢,特別適合大型或高互動性的應用程式。作為前端工程師,你可以透過以下方式利用調度器:

  • 使用 useTransition 或 useDeferredValue 來管理低優先級任務。

  • 啟用 React 18 的併發渲染(createRoot)。

  • 避免阻塞主執行緒的同步計算。