什麼是 React 的調度器(Scheduler)?
React 的調度器(Scheduler)是 React 內部的一個模組,負責管理和安排任務的執行順序與優先級。簡單來說,它就像一個「任務管理員」,決定哪些 React 更新任務(例如狀態改變、渲染更新)應該在什麼時候執行,以確保應用程式保持流暢且高效能。
React 調度器是 React 16 引入的**併發渲染(Concurrent Rendering)**功能的核心部分。它與 React 的 Fiber 架構密切相關,目的是讓 React 能夠更好地處理大量的狀態更新、複雜的元件樹,以及在不同優先級的任務之間進行協調。
為什麼需要調度器?
在早期的 React(例如 React 15 或之前),當你更新狀態(例如透過 setState),React 會立即同步地重新渲染整個元件樹。這可能導致以下問題:
-
阻塞主執行緒:如果元件樹很大,渲染過程會佔用主執行緒,導致頁面卡頓,使用者互動(例如點擊、輸入)變得遲緩。
-
無法優先處理重要任務:所有更新任務的優先級相同,無法區分哪些更新更緊急(例如使用者輸入)或哪些可以延後(例如背景資料的更新)。
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 內部的獨立模組)。以下是調度器運作的簡化流程:
-
任務創建:
-
當你呼叫 setState 或其他觸發更新的操作時,React 會創建一個更新任務。
-
這個任務會被分配一個優先級(例如使用者互動的高優先級)。
-
-
任務排程:
-
調度器將任務加入一個內部的任務佇列(Queue)。
-
根據優先級,調度器決定哪些任務需要立即執行,哪些可以延後。
-
-
時間分片執行:
-
調度器使用瀏覽器的 requestIdleCallback 或 requestAnimationFrame(在不支援的情況下會使用 setTimeout)來安排任務。
-
每個時間分片執行一小部分工作,然後檢查是否需要暫停。
-
-
檢查優先級:
-
如果有更高優先級的任務(例如使用者點擊),調度器會暫停當前任務,優先執行新的任務。
-
完成高優先級任務後,恢復被暫停的任務。
-
-
完成渲染:
- 一旦所有任務完成,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;
程式碼逐步說明
-
匯入 useTransition:
-
useTransition 是 React 提供的一個 Hook,允許你將某些更新標記為「低優先級」,交由調度器管理。
-
isPending 表示低優先級任務是否正在執行,startTransition 用來包裝低優先級的更新。
-
-
模擬大型資料:
- largeList 是一個包含 10,000 筆資料的陣列,模擬一個大型清單的過濾場景。
-
處理輸入事件:
-
當使用者輸入時,setQuery 會立即更新輸入框的狀態(高優先級,保持輸入流暢)。
-
startTransition 將清單過濾的邏輯標記為低優先級任務,交給調度器處理。
-
-
顯示過濾結果:
-
使用 filteredList 渲染過濾後的清單,並限制只顯示前 100 筆資料,避免渲染過多 DOM 元素。
-
isPending 用來顯示「正在過濾」的提示,提醒使用者背景任務正在進行。
-
執行結果
-
輸入框流暢:即使清單過濾需要較長時間(因為資料量大),輸入框的響應仍然很快,因為 setQuery 是高優先級任務。
-
清單更新稍慢:清單的過濾和渲染被標記為低優先級,調度器會在輸入框更新完成後,利用空閒時間分片執行過濾邏輯。
-
使用者體驗提升:使用者不會因為清單過濾而感到輸入卡頓,應用程式整體感覺更流暢。
如何在專案中使用調度器?
雖然 React 的調度器是內部模組,但你可以在專案中透過以下方式間接利用它的優勢:
-
啟用併發模式:
-
使用 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 的併發功能,讓調度器自動管理任務。
-
-
使用 useTransition:
- 如上述範例所示,使用 useTransition 將非緊急的更新標記為低優先級。
-
使用 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 的更新,讓輸入框保持流暢。
-
-
避免過多同步更新:
- 避免在事件處理函數中直接執行大量計算,改用 useTransition 或 useDeferredValue 將計算移到背景。
常見問題
1. 調度器會影響我的程式碼撰寫方式嗎?
一般情況下,你不需要直接操作調度器。React 的併發功能(例如 useTransition、useDeferredValue)已經封裝了調度器的功能,你只需要使用這些 API 即可。如果你在 React 18 中使用 createRoot,調度器會自動生效。
2. 什麼時候應該使用 useTransition?
當你有以下場景時,可以考慮使用 useTransition:
-
需要處理大量資料的更新(例如過濾大型清單)。
-
希望優先保證某些 UI 的響應速度(例如輸入框或按鈕)。
-
想讓背景任務不影響使用者體驗。
3. 調度器會讓應用程式變慢嗎?
不會!調度器的目的是讓應用程式更流暢。它透過分片執行和優先級管理,避免主執行緒被長時間阻塞。雖然某些低優先級任務可能延後執行,但整體使用者體驗會更好。
總結
React 的調度器(Scheduler)是 React 併發渲染的關鍵模組,負責管理任務的優先級和執行時機。它讓 React 應用程式能夠在處理複雜更新時保持流暢,特別適合大型或高互動性的應用程式。作為前端工程師,你可以透過以下方式利用調度器:
-
使用 useTransition 或 useDeferredValue 來管理低優先級任務。
-
啟用 React 18 的併發渲染(createRoot)。
-
避免阻塞主執行緒的同步計算。