淺談React19事件調(diào)度的設(shè)計思路
先說結(jié)論,React 選擇 MessageChannel 完成事件調(diào)度,是因為它:
- 屬于宏任務(wù)(不會餓死瀏覽器:JavaScript 一直占著主線程,導(dǎo)致瀏覽器一直沒有機會去做它必須做的事(渲染、響應(yīng)輸入、布局、繪制))
- 延遲極低(接近微任務(wù),但不會阻塞渲染)
- 相較于 rAF 不綁定渲染幀
- 可控、可中斷、可讓出主線程
一、React 調(diào)度和事件循環(huán)的密切聯(lián)系
1、React 在“調(diào)度”什么?
React 調(diào)度的不是「事件」, React 調(diào)度的是:Fiber 渲染任務(wù)(render work)
也就是我上篇文章說過的這些東西:
beginWorkcompleteWork- diff
- 構(gòu)建 workInProgress Fiber 樹
React Scheduler 的目標(biāo)只有是:在不阻塞瀏覽器的前提下,盡可能多地推進 Fiber 渲染進度。
所以 Scheduler 需要滿足:
- 能反復(fù)被調(diào)用
- 每次執(zhí)行一小部分
- 執(zhí)行完就“讓出主線程”
2、回憶瀏覽器事件循環(huán)
事件循環(huán)模型:
┌─────────────┐
│ 宏任務(wù)隊列(Task) │ ← setTimeout / MessageChannel / rAF callback
└─────┬───────┘
↓
執(zhí)行 JS
↓
┌─────────────┐
│ 微任務(wù)隊列(一次性清空) │ ← Promise.then / queueMicrotask
└─────┬───────┘
↓
清空所有微任務(wù)
↓
瀏覽器渲染(paint)
因此,為了滿足上述 Scheduler 的需求,我們只能選擇 Task(后續(xù)詳細說明為什么最終選擇了 MessageChannel)。
二、React Scheduler 源碼(React 19)
在 packages/scheduler/src/forks/SchedulerHostConfig.default.js
核心邏輯(簡化):
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function requestHostCallback() {
port.postMessage(null); // 用 MessageChannel 來“自我喚醒”
}
Scheduler 執(zhí)行模型:
MessageChannel 回調(diào)觸發(fā)
↓
performWorkUntilDeadline
↓
while (還有任務(wù) && 沒超時) {
執(zhí)行 Fiber work
}
↓
時間不夠 → 再發(fā)一次 MessageChannel(MessageChannel 是“下一次調(diào)度 tick”的觸發(fā)器)
三、為什么不用微任務(wù)(Promise / queueMicrotask)
假如 React 用微任務(wù)會發(fā)生什么?
Promise.resolve().then(workLoop)
問題 1:會阻塞渲染
微任務(wù)會在 paint 之前全部執(zhí)行完
意味著:
React 繼續(xù) work
→ work 里又調(diào)度微任務(wù)
→ 瀏覽器:你先別畫
→ UI 卡死
這完全就是 Fiber 的“時間切片”的對立做法。
問題 2:微任務(wù)不可中斷
- 微任務(wù)一旦開始
- 瀏覽器必須清空
- React 無法“讓出主線程”,更沒法實現(xiàn)并發(fā)渲染
四、為什么不用 setTimeout
setTimeout 的問題不是“慢”,而是“不穩(wěn)定”。
問題 1:最小延遲不可靠
- HTML 標(biāo)準(zhǔn):?最小 4ms(?HTML Living Standard — Last Updated 31 January 2026)
If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
setTimeout 在嵌套層級超過 5 層,timeout(延時)如果小于 4ms,那么則會設(shè)置為 4ms,這個時差是 React 無法接受的。
- 精度太粗(Scheduler:“當(dāng)前幀還能不能再干 2ms 的活?”)
五、為什么不用 requestAnimationFrame(rAF)
1、rAF 被綁定到“渲染幀”
一幀 ≈ 16.6ms
但 React 的目標(biāo)是:?只要主線程空一點,我就推進一點 Fiber;?而不是:“非要等下一幀”。
2、rAF 在后臺不執(zhí)行
瀏覽器會暫停 rAF(選擇性跳過渲染幀),React 更新直接“凍結(jié)”!
六、還得是 MessageChannel ~
MessageChannel 是什么?
const channel = new MessageChannel();
// 兩個頻道端口,這兩個端口可以相互通信
const port1 = channel.port1;
const port2 = channel.port2;
btn1.onclick = function(){
// port2 給 port1 發(fā)消息
port2.postMessage(content.value);
}
// port1 監(jiān)聽自己受到的消息
port1.onmessage = function(event){
console.log(`port1 收到了來自 port2 的消息:${event.data}`);
}
MessageChannel 完美規(guī)避掉上述一系列缺點:
MessageChannel + shouldYield => 時間切片。
React 并不是“無腦跑”,而是每一小段都問一句:
shouldYield()
判斷依據(jù):
- performance.now()
- 幀預(yù)算
- 用戶輸入是否 pending
如果該讓出:
requestHostCallback() // 再發(fā)一個 MessageChannel return;
[宏任務(wù)] MessageChannel
↓
React 執(zhí)行 Fiber work(2~5ms)
↓
shouldYield = true
↓
postMessage 再約一次
↓
[瀏覽器有機會 paint / 處理輸入]
↓
[下一次 MessageChannel]
七、彩蛋來咯
1、requestAnimationFrame
盲猜很多同學(xué)對于上面若干種不如 MessageChannel 的做法還不是很清楚,根本在于事件循環(huán)掌握的不好,我這里針對事件循環(huán)的**
requestAnimationFrame**詳細講講(其他知識點可以翻看我之前寫的關(guān)于事件循環(huán)的文章,講解的非常清楚)。
事件循環(huán)里面的 requestAnimationFrame 僅僅是一個跟著渲染幀走的“小弟”,有渲染才有 rAF:
- 它不能“縮短”上一個 16.66ms 中 Task 的執(zhí)行時間
- 保證回調(diào)只會在“瀏覽器即將渲染下一幀之前”執(zhí)行
因此如果上一幀的 Task 太重導(dǎo)致錯過渲染窗口,瀏覽器會直接“丟幀”,而不是排隊執(zhí)行導(dǎo)致連鎖累積卡頓(setTimeout 的做法)
rAF 回調(diào)永遠不會擠占渲染時機,只會“對齊”渲染節(jié)奏
“丟幀”這個概念,對于數(shù)碼產(chǎn)品經(jīng)常關(guān)注的同學(xué)應(yīng)該會非常熟悉。我們拿游戲“原神”舉例子,幀率越高動畫越流暢,而如果某一幀事件 Task 執(zhí)行時間太長(超過 1 幀總時長),rAF 就不再執(zhí)行,這幀就被自動“丟掉了”。而一些手機廠商為了彌補這個問題,所以就出現(xiàn)了手動“插幀”的做法。
一般地,1s 對應(yīng)著 60 幀,而 1 幀就是 16.66ms。如果一個 Task 超過了 16.66ms,那么就占用了下一幀的時間,下一幀則不再 rAF/paint (出現(xiàn)丟幀)。但如果我們使用低幀率,假如使用 30 幀 1s,那么 1 幀就是 33.3ms,這樣雖然畫質(zhì)變差了,但是動畫流暢度確實更好了。
瀏覽器在一幀內(nèi)要做的事情(簡化):
JS Task(古老說法:宏任務(wù))
→ 微任務(wù)
→ rAF
→ 樣式計算
→ Layout
→ Paint
→ Composite
→ 屏幕顯示
?只要 JS Task 超過 ~16ms,瀏覽器就來不及渲染這一幀?,結(jié)果就是:
- 這一幀直接沒畫出來(掉幀)
- 用戶看到卡頓
假設(shè)這樣寫動畫:
setTimeout(step, 16)
發(fā)生了什么?
Task A (20ms) 超過 16ms
↓
setTimeout 回調(diào)排隊
↓
Task B (又 20ms)
↓
Task C ...
后果是:
- 定時器 只管時間,不管渲染(這是“時間驅(qū)動”,不是“渲染驅(qū)動”)
- 回調(diào)會 持續(xù)排隊
- 每一幀都被 JS Task 擠爆
- 卡頓會 累積 + 放大
如果改為 rAF:
requestAnimationFrame(callback) // “當(dāng)瀏覽器準(zhǔn)備開始下一次渲染之前,調(diào)用我”
while (true) {
1. 取一個 Task 執(zhí)行(macro task)
2. 執(zhí)行所有 microtasks
3. 【渲染檢查點】(當(dāng)前時間 - 上一幀渲染時間 < 16.66ms(60Hz))
- requestAnimationFrame
- style / layout / paint
}
當(dāng)然,如果 Task 一直執(zhí)行得太久,
requestAnimationFrame一直得不到執(zhí)行,本質(zhì)上仍然是卡頓,而且是「主線程被長期占用型卡頓」。所以 rAF 并不能拯救被 JS 完全占死的主線程。
2、用時間軸演示卡頓
卡頓:場景一
類型一:JS 把主線程徹底占死(致命卡頓)
Task 200ms?Task 200ms?Task 200ms
結(jié)果:
- rAF
- Render
- 輸入響應(yīng)
- 頁面假死
rAF 無解
類型二:單幀偶爾超時(可恢復(fù)卡頓)
Task 20ms(偶發(fā))?Task 5ms?Task 5ms
結(jié)果:
- 掉 1 幀
- 后續(xù)幀恢復(fù)
- 動畫繼續(xù)
這是 rAF 的“主戰(zhàn)場”
卡頓:場景二
假設(shè)場景
- 屏幕 60Hz(16.6ms / 幀)
- 每個動畫 step 的 JS 執(zhí)行 18ms
- 使用
setTimeout(step, 16)
第 1 幀(已經(jīng)開始出問題)
0ms Task: step 執(zhí)行(18ms)?18ms microtasks?18ms ? 超過 16.6ms,無法渲染?18ms setTimeout 已經(jīng)到期 → 下一個 step 已在 Task 隊列中
結(jié)果:沒渲染,但 JS 沒停
第 2 幀(開始積壓)
18ms Task: step 執(zhí)行(18ms)?36ms microtasks?36ms ? 又錯過渲染?36ms 下一個 step 繼續(xù)排隊
第 N 幀(雪崩)
Task → Task → Task → Task → Task ? 18ms 18ms 18ms 18ms 18ms
表現(xiàn)為:
- JS 一直在跑
- 瀏覽器幾乎沒有 Render 機會
- 頁面看起來 卡住不動
- CPU 占滿
setTimeout 只認(rèn):時間到了 → 執(zhí)行回調(diào)
不管:
- 主線程忙不忙
- 能不能渲染
- 用戶是不是在滾動 / 點擊
當(dāng)一幀沒畫出來:
- rAF:直接跳過
- setTimeout:繼續(xù)補執(zhí)行(它會制造“補幀”)
這意味著:錯過的幀會變成多余的 JS 工作量
3、用戶體感 vs setTimeout
setTimeout(雪崩)
Task Task Task Task Task ?18ms 18ms 18ms 18ms
- JS 連續(xù)霸占主線程
- Render 幾乎進不去
- 頁面“僵死”
requestAnimationFrame(穩(wěn)定但慢)
step →(等下一幀)→ step →(等下一幀)→ step
- 每幀最多執(zhí)行一次
- Render 之間有喘息
- 頁面還能響應(yīng)輸入
- 動畫只是 低 FPS(這是“慢”,不是“死”)
到此這篇關(guān)于淺談React19事件調(diào)度的設(shè)計思路的文章就介紹到這了,更多相關(guān)React19事件調(diào)度內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在?React?中如何從狀態(tài)數(shù)組中刪除一個元素
這篇文章主要介紹了在?React?中從狀態(tài)數(shù)組中刪除一個元素,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03
TypeScript在React項目中的實戰(zhàn)應(yīng)用指南及技巧
在React項目中集成TypeScript可以顯著提升開發(fā)體驗,通過類型檢查減少運行時錯誤,提高代碼可維護性,這篇文章主要介紹了TypeScript在React項目中實戰(zhàn)應(yīng)用指南及技巧的相關(guān)資料,需要的朋友可以參考下2026-03-03
React-Native實現(xiàn)ListView組件之上拉刷新實例(iOS和Android通用)
本篇文章主要介紹了React-Native實現(xiàn)ListView組件之上拉刷新實例(iOS和Android通用),具有一定的參考價值,有興趣的可以了解一下2017-07-07
react函數(shù)組件useState異步,數(shù)據(jù)不能及時獲取到的問題
這篇文章主要介紹了react函數(shù)組件useState異步,數(shù)據(jù)不能及時獲取到的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
淺析JS中什么是自定義react數(shù)據(jù)驗證組件
我們在做前端表單提交時,經(jīng)常會遇到要對表單中的數(shù)據(jù)進行校驗的問題。這篇文章主要介紹了js中什么是自定義react數(shù)據(jù)驗證組件,需要的朋友可以參考下2018-10-10

