競態問題/競態條件 指的是,當我們在交互過程中,由于各種原因導致同一個接口短時間之內連續發送請求,后發送的請求有可能先得到請求結果,從而導致數據渲染出現預期之外的錯誤。
因為防止重復執行可以有效的解決競態問題,因此許多時候面試官也會直接在面試中問我們如何實現防重。常用的方式就是取消上一次請求,或者設置狀態讓按鈕不能連續點擊,想必各位大佬對這些方案都已經非常熟悉,我這里就不展開細說。
React 19 結合 Suspense 也在競態問題上,提出了一個自己的解決方案。我們結合新的案例來探討一下這個問題,看完之后大家感受一下這種方式是好是壞。
const getApi = async () => {const res = await fetch('https://api.chucknorris.io/jokes/random')return res.json()
}export default function Index() {const [api, setApi] = useState(null)const [list, setList] = useState([])function __clickToGetMessage() {setApi(getApi())}return (<div><div id='tips'>點擊按鈕新增一條數據,該數據從接口中獲取</div><button onClick={__clickToGetMessage}>新增數據</button><div className="content"><div className="list">{list.map((item, index) => {return <div className='item' key={item}>{item}</div>})}</div><Suspense fallback={<div>loading...</div>}><Item api={api} setList={setList} /></Suspense></div></div>)
}const Item = ({api, setList}) => {const [show, setShow] = useState(true)const joke = api ? use(api) : {value: 'nothing'}useEffect(() => {if (!api) returnsetList((list) => {if (!list.includes(joke.value)) {return list.concat(joke.value)}return list})setShow(false)}, [])const __cls = show ? '_03_a_value show' : '_03_a_value'return (<div className={__cls}>{joke.value}</div>)
}
首先,多次點擊會導致多次請求,因此數組中會新增大量的數據。
其次,由于請求太密集,那么點擊的先后順序,與請求成功的先后順序不一致,因此列表中的順序也會與點擊順序不同。「競態問題」
那么我們來試著操作一下,看看該案例會有什么反應。演示結果如下,新增一條數據時,我連續點擊了 10 次。
?
結果我們發現,點擊期間,并沒有新的數據渲染到頁面上,一直是 loading 的狀態。
再來看一下此時的請求情況。
請求的順序被嚴格控制了:上一個請求請求成功之后,下一個請求才開始發生。此時是一個串行的請求過程。
react 19 使用這種思路解決了競態問題。與此同時,反饋到數據上,雖然前面多次的請求已經成功,但是對于組件狀態來說,這個中間過程中一直有請求在發生,此時 React 認為中間的請求產生的數據為無效數據。只會把最后一個請求成功的數據作為最終的返回結果。
?
很顯然,僅從 UI 結果上來說,這樣的處理方式確實是非常合理的,我們不需要過多的干涉數據的處理,非常的輕松。但問題是,每次請求都成功發生。
當我點擊 10 次,就會有 10 次請求,由于使用串行的策略來解決競態問題,導致最后一次的請求結果需要等待很長實踐才會返回。這無疑極大的降低了開發體驗。
和取消上一次的請求相比,無論是從體驗上,還是從效率上來說,無疑都是更差的一種方案。
和以往的解決方案,如按鈕點擊后在請求結果回來之前禁用按鈕點擊,或取消上一次請求相比,體驗要差一點。