React處理高頻的實時數(shù)據(jù)的解決方案
最近,我遇到了一個很有意思的 React 問題。
我需要開發(fā)一個實時的日志查看器,功能上需要實時展示服務(wù)運行的日志。因為這個項目是內(nèi)部的,我這里大概抽象一下:
后端使用 SSE(Server-Sent Events) 技術(shù),源源不斷地把日志推送給前端。
當(dāng)日志一條一條、不緊不慢地過來時,一切正常。
但是,當(dāng)我預(yù)覽一個已經(jīng)完成的任務(wù)日志時,網(wǎng)頁卡頓了一下。瀏覽器控制臺顯示了一個 React 開發(fā)者很熟悉的錯誤:
Uncaught Error: Maximum update depth exceeded... (錯誤:超過最大更新深度)
這個錯誤通常意味著,存在什么組件陷入了無限循環(huán)。比如,組件的渲染函數(shù)里直接調(diào)用了 setState,導(dǎo)致“渲染 → 更新狀態(tài) → 觸發(fā)渲染 → ...”的死循環(huán)。
比如這樣:
export default function Demo() {
const [count, setCount] = useState(0)
setCount(count + 1)
return <h1>Count: {count}</h1>
}
但我的代碼并沒有這樣的邏輯,該使用 useEffect 的地方都使用了。我只是在 SSE 的事件回調(diào)里更新狀態(tài)。
// 示意代碼
const source = new EventSource("/api/logs")
source.addEventListener("log", (event) => {
// 每來一條日志,就調(diào)用 set 函數(shù)
appendLog(event.data)
})
那么,問題出在哪里呢?
問題的根源:高頻更新
起初我以為是哪里的更新邏輯不對,讓 claude 排查很久都沒找到具體問題。在給現(xiàn)有函數(shù)增加了不少緩存,比如useMemo,useCallback,甚至 React.memo 都使用上了,仍舊沒有解決這個報錯。
代碼沒有問題,那么問題就應(yīng)該出現(xiàn)在一些極端場景導(dǎo)致的高頻渲染。比如網(wǎng)絡(luò)?我才打開控制臺的網(wǎng)絡(luò)部分,看到幾乎在很短時間內(nèi),上百條的 log 被推送過來!
到這里問題就和清晰了:當(dāng)服務(wù)器在短時間內(nèi)(比如 1 秒內(nèi))推送上百條日志時,每一個 log 都觸發(fā)了 React 進行重新渲染,這里觸發(fā)了 React 的某些機制,React 對這種行為發(fā)出了報錯。
React 內(nèi)部有一個“嵌套更新計數(shù)器”,用來防止無限循環(huán)。
簡單說,如果在一次渲染(Render)的過程中,又因為某些原因觸發(fā)了新的狀態(tài)更新,這就叫“嵌套更新”。當(dāng)這個次數(shù)短時間內(nèi)超過一個閾值(通常是 50 次),React 就會認為你“可能”寫了一個 Bug,于是主動拋出錯誤,終止程序。
我們的問題就出在這里。SSE 的事件回調(diào)來得太快了。
當(dāng)服務(wù)器在 1 秒內(nèi)推送 150 條日志時,瀏覽器的事件循環(huán)會瘋狂執(zhí)行回調(diào):
- SSE 事件 1 抵達 →
appendLog()→ 觸發(fā) React 更新(第 1 次) - React 還沒來得及渲染,SSE 事件 2 抵達 →
appendLog()→ 觸發(fā) React 更新(第 2 次) - ...
- SSE 事件 50 抵達 →
appendLog()→ 觸發(fā) React 更新(第 50 次) - SSE 事件 51 抵達 →
appendLog()→ 觸發(fā) React 更新(第 51 次)
在 React 看來,這 51 次更新幾乎是“同時”發(fā)生的,它無法分辨這是“51 條獨立日志”還是“一個死循環(huán)”。為了保護自己,它選擇了報錯。
問題的本質(zhì)是:數(shù)據(jù)接收的頻率(高頻)和 React 狀態(tài)更新的頻率(低頻)不匹配。
我們不能每收到一條數(shù)據(jù),就立刻更新一次狀態(tài)。
后續(xù)我了解到 React 18 版本對高頻渲染的問題進行了優(yōu)化,但它目前僅適用于 React 事件處理函數(shù)內(nèi)的同步更新。對于 SSE 回調(diào)、fetch 回調(diào)、setInterval 等異步事件源觸發(fā)的更新,仍需手動實現(xiàn)批處理。
解決方案:批處理(Batching)
既然不能一條一條地更新,那很自然就想到,能不能把日志“攢一下”,再一次性提交給 React?
這就是“批處理”(Batching)思想。
我們不再是“來一條,更新一次”,而是“來 N 條,更新一次”。
實現(xiàn)這個功能的關(guān)鍵,是需要一個“緩沖區(qū)”(Buffer)和一個“定時器”(Timer)。
- 緩沖區(qū):需要一個地方暫存日志,但這個地方本身不能是 React 的
state(否則又觸發(fā)渲染了)。useRef是最合適的人選。 - 定時器:需要一個機制,在“攢”日志的間隙,把它們統(tǒng)一提交。
setTimeout(..., 0)是這里的法寶。
代碼實現(xiàn)
我們來改造一下 log 事件的處理。
首先,在組件里定義緩沖區(qū)和定時器:
export default function LogPage() {
// 1. 從 store 獲取批量更新的方法
const appendLogs = useLogStore((state) => state.appendLogs)
// 2. 批處理緩沖區(qū)(使用 ref 不會觸發(fā)渲染)
const batchBufferRef = useRef([])
// 3. 定時器引用(保證只有一個定時器在運行)
const batchTimerRef = useRef(null)
// ...
}
其次,實現(xiàn)一個“提交緩沖區(qū)”的函數(shù) flushBatch:
// 4. 批量提交函數(shù)
const flushBatch = useCallback(() => {
// 如果緩沖區(qū)有數(shù)據(jù)
if (batchBufferRef.current.length > 0) {
// 一次性提交給 store
appendLogs(batchBufferRef.current)
// 清空緩沖區(qū)
batchBufferRef.current = []
}
// 重置定時器引用
batchTimerRef.current = null
}, [appendLogs]) // 依賴 appendLogs
最后,修改 SSE 的事件處理函數(shù) handleLogEvent:
// 5. 新的 SSE 事件處理函數(shù)
const handleLogEvent = useCallback(
(event) => {
const entry = {
/* ...解析日志... */
}
// 重點:不再直接調(diào)用 appendLog
// 而是將日志加入緩沖區(qū)
batchBufferRef.current.push(entry)
// 如果還沒有計劃批處理,則在下一個事件循環(huán)中執(zhí)行
if (batchTimerRef.current === null) {
batchTimerRef.current = window.setTimeout(flushBatch, 0)
}
},
[flushBatch] // 依賴 flushBatch
)
為什么是setTimeout(..., 0)?
你可能會問,為什么是 setTimeout(..., 0)?
這是一個很巧妙的技巧。它并不是真的“延遲 0 毫秒”,而是告訴瀏覽器:“請在當(dāng)前這一輪事件循環(huán)(Event Loop)的同步代碼都執(zhí)行完之后,再執(zhí)行這個 flushBatch 函數(shù)。”
當(dāng) 150 條日志在短時間內(nèi)涌入時,會發(fā)生什么?
- 事件 1 抵達 →
push到緩沖區(qū) →setTimeout注冊一個flushBatch回調(diào)。 - 事件 2 抵達 →
push到緩沖區(qū) → 檢查定時器,發(fā)現(xiàn)已有,跳過。 - 事件 3 抵達 →
push到緩沖區(qū) → 跳過。 - ...
- 事件 150 抵達 →
push到緩沖區(qū) → 跳過。 - (當(dāng)前宏任務(wù)結(jié)束,所有同步代碼執(zhí)行完畢)
- 瀏覽器從任務(wù)隊列中取出
flushBatch回調(diào),執(zhí)行。 flushBatch函數(shù)將 150 條日志一次性提交給 React。
于是,150 次 setState 調(diào)用,被神奇地合并成了 1 次。應(yīng)用流暢如初。
到此這篇關(guān)于React處理高頻的實時數(shù)據(jù)的解決方案的文章就介紹到這了,更多相關(guān)React處理高頻實時數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React使用useSearchParams同步URL和查詢參數(shù)的方法
在開發(fā)React應(yīng)用時,我們經(jīng)常遇到一種場景:用戶在搜索框輸入關(guān)鍵詞,篩選出一個列表,然后希望把這個結(jié)果分享給同事,在React Router v6中,useSearchParams這個Hook就是專門用來處理這個問題的,本文將介紹如何使用它來實現(xiàn) URL 與應(yīng)用狀態(tài)的同步,需要的朋友可以參考下2025-12-12
React?高階組件與Render?Props優(yōu)缺點詳解
這篇文章主要weidajai?介紹了React?高階組件與Render?Props優(yōu)缺點詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11
在React頁面重新加載時保留數(shù)據(jù)的實現(xiàn)方法總結(jié)
在React頁面重新加載時保留數(shù)據(jù),可以通過多種方法來實現(xiàn),常見的方法包括使用瀏覽器的本地存儲(Local Storage 或 Session Storage)、URL參數(shù)、以及服務(wù)器端存儲等,本文給大家總結(jié)了一些具體實現(xiàn)方法,需要的朋友可以參考下2024-06-06
React組件三大核心屬性State?props?Refs介紹
組件實例的三大核心屬性是:State、Props、Refs。類組件中這三大屬性都存在。函數(shù)式組件中訪問不到?this,也就不存在組件實例這種說法,但由于它的特殊性(函數(shù)可以接收參數(shù)),所以存在Props這種屬性2023-02-02
React 全自動數(shù)據(jù)表格組件——BodeGrid的實現(xiàn)思路
表格是在后臺管理系統(tǒng)中用的最頻繁的組件之一,相關(guān)的功能有數(shù)據(jù)的新增和編輯、查詢、排序、分頁、自定義顯示以及一些操作按鈕。這篇文章主要介紹了React 全自動數(shù)據(jù)表格組件——BodeGrid ,需要的朋友可以參考下2019-06-06

