在前一篇原子交易(Atomic Transaction)中,我們已經看過交易如何確保狀態一致性。
這一篇要更深入探討 Scheduler(調度器)的設計,說明不同策略的取捨,並對照我們的程式碼在流程圖中落在哪條路徑。
Scheduler 是 reactivity 系統中的「隱形指揮官」
簡單來說,Scheduler 不是「要不要算」,而是「何時算」。
大致可以分為以下幾類:
這種差異會影響系統的整體策略:
這張圖展示了從「資料更新」到「UI 更新」的不同策略路徑。
策略 | 觸發時機 | 核心資料流 | 優點 | 代價 / 風險 | 適用情境 | 常見實例 |
---|---|---|---|---|---|---|
同步調度 (Sync) | set() 後立即執行 |
write → compute → effect |
心智直觀、除錯容易 | 重複運算、互動抖動 | 極簡頁面、更新頻率低 | 小型框架、教學用 PoC |
批次調度 (Batch) | 同一 tick/microtask 末尾一次 flush | write × N → queue → flush |
減少重繪、效能佳 | 延遲一致、需要 queue | 表單互動、動畫外層 | React batched updates、Vue job queue |
優先級調度 (Priority) | 依 deadline / 重要性切片 | enqueue(prio) → run until deadline |
不阻塞高價值互動 | 複雜度高、可能「飢餓」 | 長列表、並行渲染 | React Concurrent、startTransition |
Lazy | 被讀取時才計算 | write → mark-dirty → read 才算 |
寫入代價低 | 首次讀取延遲 | 讀多寫少 | Signals memo /computed |
Eager | 寫入當下標記下游 | write → mark downstream → flush |
讀取更快、失效清楚 | 寫入代價高 | 寫少讀多、強一致 UI | Solid/Preact Signals |
框架 | 調度模式 | 策略 |
---|---|---|
React | Batch + Priority | microtask queue + concurrent |
Vue 3 | Batch | job queue |
Solid | Microtask Batch (Eager) | 每個更新皆標記並批次刷新 |
Signals (Preact/Angular) | Push + Lazy | 僅在讀取時才計算依賴 |
可以看出不同框架對 Scheduler 的取捨,直接影響到「效能優化」與「心智模型」的差異。
在 Atomic Transaction 章節,我們有三個重要情境:
begin → writes → commit
資料更新 → 批次調度 + Eager 標記 → flush → 副作用
begin → writes → rollback
資料更新 → (尚未 flush) → 回滾 → 標髒下游 → 下次讀取才重算
signal.set()
→ 寫入markStale(node)
→ Eager 標記track()/link()/unlink()
→ 依賴追蹤queueMicrotask(flush)
→ Batch flushflush()
→ 拓撲重算 + runEffectsrollback()
→ 還原快照 + 標記髒值atomic()
/ transaction()
在上一篇中,為了方便大家對照,所以我保留了原本 transaction()
的部分,但其實應該要使用 atomic()
的邏輯取代掉 transaction()
的,從程式碼來看,具備完整交易語意的是 atomic()
:
flushJobs()
(屬於 Batch + Eager)。而目前的 transaction()
僅等同於「批次 (batch)」,缺乏 rollback,語意與前文不同。
transaction
與前文語意一致,建議直接包裝成 atomic()
的別名:export function transaction<T>(fn: () => T): T;
export function transaction<T>(fn: () => Promise<T>): Promise<T>;
export function transaction<T>(fn: () => T | Promise<T>): T | Promise<T> {
return atomic(fn);
}
scheduleJob()
加入 muted
判斷,避免 rollback 期間排進新任務:export function scheduleJob(job: Schedulable) {
if (job.disposed) return;
if (muted > 0) return; // 回滾/靜音期間不進隊列
queue.add(job);
if (!scheduled && batchDepth === 0) {
scheduled = true;
queueMicrotask(flushJobs);
}
}
其他部分不需要更動,改了這個只是讓行為跟上述策略描述上對齊。
一個有趣的問題是:
開發者究竟需不需要知道 Scheduler 的存在?
因此,我們可以這樣總結:
Scheduler 不僅是效能優化的工具,也是一種「開發者體驗」的設計。
進階的 Scheduler 可能還會包含:
這些設計概念其實與作業系統的「排程器」如出一轍,只是應用在前端 reactivity 的脈絡中。
Scheduler 在 reactivity 系統中,扮演著「隱形指揮官」的角色。
它不僅決定了效能的上限,也影響了開發者的心智模型。
在不同框架的取捨背後,其實都圍繞著同一個問題:
「我們希望在什麼時機,付出多少代價,去換取 UI 的即時一致性?」
下一篇,我們來談談 Scheduler 在記憶體與圖管理上面臨的議題。