深度解析React18 flushSync的使用
前言
React18 引入了并發(fā)渲染與全局自動批處理,默認(rèn)情況下所有setState都會異步合并渲染優(yōu)化性能。但部分業(yè)務(wù)需要修改狀態(tài)后立刻拿到最新DOM,官方提供了flushSync強(qiáng)制同步刷新API。本文覆蓋所有執(zhí)行場景、陷阱、優(yōu)先級、異常、對比、面試題
導(dǎo)入方式:import { flushSync } from 'react-dom'
一、基礎(chǔ)介紹
1. 核心作用
強(qiáng)制同步執(zhí)行回調(diào)內(nèi)狀態(tài)更新,跳過并發(fā)調(diào)度、批處理機(jī)制,同步走完render → commit → DOM真實更新全流程。
2. 基礎(chǔ)示例
const [count, setCount] = useState(0)
const handle = () => {
flushSync(() => {
setCount(1)
})
// 此處DOM已經(jīng)刷新完成,可讀取offset、焦點等
}
3. 適用場景
- 修改狀態(tài)后立即讀取DOM寬高、位置、滾動距離
- 渲染完成馬上控制輸入框焦點
- 對接圖表、動畫等第三方庫,必須依賴最新DOM實例
- 多步驟邏輯強(qiáng)依賴上一步渲染結(jié)果
4. 禁止使用場景
- 組件渲染函數(shù)體內(nèi)直接調(diào)用
useEffect、useLayoutEffect內(nèi)部調(diào)用(會控制臺警告)- 無必要場景濫用,會破壞并發(fā)渲染、降低頁面性能
二、全場景執(zhí)行邏輯拆解
場景1:flushSync前置存在普通setState
規(guī)則
執(zhí)行flushSync時,會優(yōu)先清空之前所有堆積未渲染的更新隊列,前置更新單獨渲染一次,內(nèi)部更新合并再渲染一次。
const [n, setN] = useState(0)
const run = () => {
setN(1) // 緩存入隊,暫不渲染
flushSync(() => {
setN(prev => prev + 2)
})
console.log(n) // 輸出3
}
執(zhí)行流程:
setN(1)進(jìn)入待更新隊列- 進(jìn)入flushSync,先沖刷前置隊列,n=1,第一次同步渲染
- 內(nèi)部
setN(prev+2)存入同步批次 - 回調(diào)執(zhí)行完畢批量渲染,n=3,第二次渲染
場景2:flushSync內(nèi)部多個setState
規(guī)則
同一個flushSync回調(diào)內(nèi)多個狀態(tài)更新,自動合并批量,只同步渲染1次。區(qū)別普通更新:普通是異步批處理,這里是同步批處理。
flushSync(() => {
setCount(1)
setName('demo')
setVisible(true)
})
// 三個狀態(tài)一次性同步更新DOM,僅渲染一次
場景3:flushSync嵌套調(diào)用
底層鐵律
同一時間只能存在一個同步刷新上下文;同步棧內(nèi)嵌套的flushSync直接失效,僅執(zhí)行回調(diào),不會新增渲染次數(shù)。
flushSync(() => {
setCount(p => p + 1)
// 內(nèi)層全部失效,無獨立刷新
flushSync(() => {
setCount(p => p + 2)
flushSync(() => setCount(p => p + 3))
})
})
// 總計只渲染1次,最終count=6
嵌套問題解決方案
想要分步多次同步刷新,禁止嵌套,拆分為平級獨立flushSync:
flushSync(() => setA(1)) // 第一次渲染 flushSync(() => setB(2)) // 第二次渲染 flushSync(() => setC(3)) // 第三次渲染
場景4:flushSync內(nèi)部包含異步代碼(async/await/setTimeout/Promise)
核心結(jié)論
只有同步執(zhí)行棧內(nèi)的setState受flushSync管控;一旦進(jìn)入異步回調(diào)(定時器、then、await后),狀態(tài)更新脫離同步上下文,回歸默認(rèn)異步批處理。
- setTimeout宏任務(wù)示例
flushSync(() => {
setNum(1)
setTimeout(() => {
setNum(2) // 異步隊列,延遲渲染,不跟隨同步刷新
}, 0)
})
// 外層DOM先變?yōu)?,宏任務(wù)執(zhí)行后才變?yōu)?
- async/await陷阱(極易踩坑)
const fn = async () => {
flushSync(async () => {
setN(1)
await Promise.resolve() // 讓出同步棧,后續(xù)代碼變成微任務(wù)
setN(2) // 不受flushSync管控,異步渲染
})
}
- 異步回調(diào)內(nèi)新開flushSync
異步執(zhí)行棧中創(chuàng)建的flushSync是全新獨立同步上下文,可以正常強(qiáng)制刷新:
flushSync(() => {
setNum(1)
setTimeout(() => {
flushSync(() => setNum(2)) // 獨立生效,同步渲染
}, 0)
})
場景5:flushSync內(nèi)部拋出異常
規(guī)則
回調(diào)內(nèi)報錯不會回滾已執(zhí)行的狀態(tài)更新,已經(jīng)執(zhí)行的setState依舊會同步渲染;錯誤同步拋出,可用try/catch捕獲。
const [n, setN] = useState(0)
const test = () => {
try {
flushSync(() => {
setN(1) // 正常渲染生效
throw new Error('執(zhí)行異常')
setN(2) // 不會執(zhí)行
})
} catch (err) {}
console.log(n) // 打印1
}
場景6:flushSync搭配startTransition(優(yōu)先級對比)
優(yōu)先級排序:flushSync(最高同步) > 普通更新 > startTransition(低優(yōu)先級過渡)
flushSync會插隊優(yōu)先執(zhí)行,transition內(nèi)更新延后渲染:
const [n, setN] = useState(0)
const run = () => {
startTransition(() => setN(10)) // 低優(yōu)先級延后
flushSync(() => setN(1)) // 最高優(yōu)先級立刻渲染
console.log(n) // 打印1
}
場景7:useLayoutEffect / useEffect中調(diào)用flushSync
渲染周期鉤子內(nèi)部調(diào)用會觸發(fā)React警告,屬于不規(guī)范寫法。渲染階段本身正在執(zhí)行更新流程,無法二次開啟同步刷新上下文。
// 警告不推薦
useLayoutEffect(() => {
flushSync(() => setCount(1))
}, [])
場景8:實戰(zhàn)標(biāo)準(zhǔn)DOM讀取場景
業(yè)務(wù)最常用:先展示隱藏元素,立刻讀取尺寸計算動畫、定位:
const boxRef = useRef(null)
const openBox = () => {
flushSync(() => setShow(true))
// DOM已渲染完成,精準(zhǔn)獲取高度
const height = boxRef.current.offsetHeight
}
三、經(jīng)典閉包快照陷阱
flushSync僅同步DOM視圖,組件內(nèi)state變量為函數(shù)執(zhí)行瞬間的閉包快照,不會實時變化;狀態(tài)累加務(wù)必使用函數(shù)式更新setX(prev => prev + val)。
const [val, setVal] = useState(0)
const run = () => {
setVal(1)
flushSync(() => {
console.log(val) // 0,閉包捕獲舊快照
setVal(p => p + 2) // 函數(shù)式更新不受快照影響
})
}
四、重要注意事項(避坑清單)
- 杜絕濫用:強(qiáng)制同步渲染會打斷React并發(fā)機(jī)制,頻繁調(diào)用造成頁面卡頓
- 同步棧內(nèi)禁止嵌套,分步刷新只能平級書寫
- 不要給flushSync傳遞async回調(diào),await會撕裂同步執(zhí)行棧
- 異步邏輯如需同步DOM,在異步回調(diào)內(nèi)部單獨包裹flushSync
- 循環(huán)中批量調(diào)用flushSync會產(chǎn)生多次重渲染,性能損耗極大
- 遇到Suspense時,flushSync強(qiáng)制同步會直接展示fallback加載態(tài),無法等待異步資源
- 狀態(tài)累加統(tǒng)一使用函數(shù)式更新,規(guī)避閉包快照數(shù)值錯誤
五、flushSync vs Vue nextTick 對比
| 對比項 | React flushSync | Vue nextTick |
|---|---|---|
| 核心行為 | 主動強(qiáng)制立刻同步刷新DOM | 被動等待DOM更新完畢再執(zhí)行回調(diào) |
| 執(zhí)行類型 | 同步阻塞代碼 | 微任務(wù)異步執(zhí)行 |
| 批處理影響 | 打破React默認(rèn)自動批處理 | 不改變Vue自身批處理邏輯 |
| 使用目的 | 修改state馬上拿到最新DOM | 修改數(shù)據(jù)后等渲染完成操作DOM |
| 兩者關(guān)系 | 邏輯完全相反,不能互相替代 | 邏輯完全相反,不能互相替代 |
六、高頻面試真題(含完整答案)
面試題1 前置setState+內(nèi)部多更新
const [n, setN] = useState(0);
function handleClick() {
setN(1);
flushSync(() => {
setN(prev => prev + 2);
setN(prev => prev + 3);
});
console.log(n);
}
答案:打印6;總渲染2次(前置1次、內(nèi)部合并1次)
面試題2 同步多層嵌套flushSync
const [num, setNum] = useState(0);
function test() {
flushSync(() => {
setNum(1);
flushSync(() => {
setNum(2);
flushSync(() => {
setNum(3);
});
});
});
console.log(num);
}
答案:打印3;僅渲染1次,內(nèi)層嵌套全部失效
面試題3 壓軸綜合大題
const [count, setCount] = useState(0);
function handleClick() {
setCount(1);
flushSync(() => {
setCount(p => p + 1);
flushSync(() => {
setCount(p => p + 2);
flushSync(() => {
setCount(p => p + 3);
});
});
});
flushSync(() => {
setCount(p => p + 4);
});
console.log(count);
}
問題1:最終打印數(shù)值?問題2:原始代碼渲染幾次?問題3:第二個平級flushSync挪入最深嵌套渲染幾次?
答案:
- 1+1+2+3+4 = 11,打印11
- 原始渲染3次:前置1次、嵌套批量1次、平級第二個獨立1次
- 移入嵌套后全程單同步上下文,僅渲染2次
面試題4 異常場景
const [n, setN] = useState(0);
function test() {
try {
flushSync(() => {
setN(1);
throw Error('err');
});
} catch (e) {}
console.log(n);
}
答案:打印1,異常不會回滾已執(zhí)行狀態(tài)
面試題5 優(yōu)先級startTransition對比
const [n, setN] = useState(0);
function test() {
startTransition(() => setN(10));
flushSync(() => setN(1));
console.log(n);
}
答案:打印1,flushSync優(yōu)先級高于transition
七、開發(fā)最佳實踐
- 優(yōu)先使用
useLayoutEffect派生數(shù)據(jù)、操作DOM,非必要不用flushSync - 接口請求、異步邏輯放在flushSync外部,拿到結(jié)果后再同步刷新視圖
- 同一批次無關(guān)多狀態(tài)統(tǒng)一放入單個flushSync,減少渲染次數(shù)
- 多層分步DOM操作拆分平級flushSync,不依賴嵌套邏輯
- 所有數(shù)值疊加更新全部采用函數(shù)式
setX(prev => prev + value)寫法
總結(jié)
- flushSync為最高優(yōu)先級同步刷新,前置隊列會提前沖刷
- 同回調(diào)內(nèi)多setState合并一次渲染;同步棧嵌套內(nèi)層失效
- 異步代碼脫離同步上下文,只有異步內(nèi)新開flushSync才可再次同步
- 異常、閉包、transition、生命周期均有固定執(zhí)行規(guī)則
- 和Vue nextTick行為完全相反,一個強(qiáng)制立刻刷,一個等待刷完再執(zhí)行
- 謹(jǐn)慎使用,僅DOM讀取、焦點、第三方庫兼容場景作為兜底方案
到此這篇關(guān)于深度解析React18 flushSync的使用的文章就介紹到這了,更多相關(guān)React18 flushSync內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript React如何修改默認(rèn)端口號方法詳解
這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號方法詳解,文中通過步驟圖片解析介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
如何解決React useEffect鉤子帶來的無限循環(huán)問題
本文主要介紹了解決React useEffect鉤子帶來的無限循環(huán)問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
react-router-domV6版本的路由和嵌套路由寫法詳解
本文主要介紹了react-router-domV6版本的路由和嵌套路由寫法詳解,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03
深入理解React調(diào)度(Scheduler)原理
本文主要介紹了深入理解React調(diào)度(Scheduler)原理,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
使用React18和WebSocket構(gòu)建實時通信功能詳解
WebSocket是一種在Web應(yīng)用中實現(xiàn)雙向通信的協(xié)議,它允許服務(wù)器主動向客戶端推送數(shù)據(jù),而不需要客戶端發(fā)起請求,本文將探索如何在React?18應(yīng)用中使用WebSocket來實現(xiàn)實時通信,感興趣的可以了解下2024-01-01

