React競(jìng)態(tài)條件Race Condition實(shí)例詳解
競(jìng)態(tài)條件
Race Condition,中文譯為競(jìng)態(tài)條件,旨在描述一個(gè)系統(tǒng)或者進(jìn)程的輸出,依賴于不受控制事件的出現(xiàn)順序或者出現(xiàn)時(shí)機(jī)。
舉個(gè)簡(jiǎn)單的例子:
if (x == 5) // The "Check" { y = x * 2; // The "Act" // 如果其他的線程在 "if (x == 5)" and "y = x * 2" 執(zhí)行之間更改了 x 的值 // y 就可能不等于 10. }
你可能想,JavaScript 是單線程,怎么可能出現(xiàn)這個(gè)問(wèn)題?
React 與競(jìng)態(tài)條件
確實(shí)如此,但前端有異步渲染,所以競(jìng)態(tài)條件依然有可能出現(xiàn),我們舉個(gè) React 中常見的例子。
這是一個(gè)非常典型的數(shù)據(jù)獲取代碼:
class Article extends Component { state = { article: null }; componentDidMount() { this.fetchData(this.props.id); } async fetchData(id) { const article = await API.fetchArticle(id); this.setState({ article }); } // ... }
看起來(lái)沒什么問(wèn)題,但這段代碼還沒有實(shí)現(xiàn)數(shù)據(jù)更新,我們?cè)俑囊幌拢?/p>
class Article extends Component { state = { article: null }; componentDidMount() { this.fetchData(this.props.id); } componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { this.fetchData(this.props.id); } } async fetchData(id) { const article = await API.fetchArticle(id); this.setState({ article }); } // ... }
當(dāng)組件傳入新的 id
時(shí),我們根據(jù)新的 id
請(qǐng)求數(shù)據(jù),然后 setState
最新獲取的數(shù)據(jù)。
這時(shí)就可能出現(xiàn)競(jìng)態(tài)條件,比如用戶選完立刻點(diǎn)擊下一頁(yè),我們請(qǐng)求 id
為 1 的數(shù)據(jù),緊接著請(qǐng)求 id
為 2 的數(shù)據(jù),但因?yàn)榫W(wǎng)絡(luò)或者接口處理等原因,id
為 2 的接口提前返回,便會(huì)先展示 id
為 2 的數(shù)據(jù),再展示 id
為 1 的數(shù)據(jù),這就導(dǎo)致了錯(cuò)誤。
我們可以想想遇到這種問(wèn)題的場(chǎng)景,比如類似于百度的搜索功能,切換 tab 等場(chǎng)景,雖然我們也可以使用諸如 debounce 的方式來(lái)緩解,但效果還是會(huì)差點(diǎn),比如使用 debounce,用戶在輸入搜索詞的時(shí)候,展示內(nèi)容會(huì)長(zhǎng)期處于空白狀態(tài),對(duì)于用戶體驗(yàn)而言,我們可以做的更好。
那么我們?cè)撊绾谓鉀Q呢?一種是在切換的時(shí)候取消請(qǐng)求,還有一種是借助一個(gè)布爾值來(lái)判斷是否需要更新,比如這樣:
function Article({ id }) { const [article, setArticle] = useState(null); useEffect(() => { let didCancel = false; async function fetchData() { const article = await API.fetchArticle(id); // 如果 didCancel 為 true 說(shuō)明用戶已經(jīng)取消了 if (!didCancel) { setArticle(article); } } fetchData(); // 執(zhí)行下一個(gè) effect 之前會(huì)執(zhí)行 return () => { didCancel = true; }; }, [id]); // ... }
當(dāng)然你也可以用 ahooks 中的 useRequest
,它的內(nèi)部有一個(gè) ref 變量記錄最新的 promise,也可以解決 Race Condition 的問(wèn)題:
function Article({ id }) { const { data, loading, error} = useRequest(() => fetchArticle(id), { refreshDeps: [id] }); // ... }
效果演示
問(wèn)題復(fù)現(xiàn)
為了方便大家自己測(cè)試這個(gè)問(wèn)題,我們提供相對(duì)完整的代碼。以 《Avoiding Race Conditions when Fetching Data with React Hooks》中的例子為例,出現(xiàn) Race Condition 問(wèn)題的代碼如下:
const fakeFetch = person => { return new Promise(res => { setTimeout(() => res(`${person}'s data`), Math.random() * 5000); }); }; const App = () => { const [data, setData] = useState(''); const [loading, setLoading] = useState(false); const [person, setPerson] = useState(null); useEffect(() => { setLoading(true); fakeFetch(person).then(data => { setData(data); setLoading(false); }); }, [person]); const handleClick = (name) => () => { setPerson(name) } return ( <Fragment> <button onClick={handleClick('Nick')}>Nick's Profile</button> <button onClick={handleClick('Deb')}>Deb's Profile</button> <button onClick={handleClick('Joe')}>Joe's Profile</button> {person && ( <Fragment> <h1>{person}</h1> <p>{loading ? 'Loading...' : data}</p> </Fragment> )} </Fragment> ); };
我們實(shí)現(xiàn)了一個(gè) fakeFetch
函數(shù),用于模擬接口的返回,具體返回的時(shí)間為 Math.random() * 5000)
,用于模擬數(shù)據(jù)的隨機(jī)返回。
實(shí)現(xiàn)效果如下:
從效果圖中可以看到,我們按順序點(diǎn)擊了 Nick
、Deb
、Joe
,理想情況下,結(jié)果應(yīng)該顯示 Joe's Data
,但最終顯示的數(shù)據(jù)為最后返回的 Nick's Data
。
布爾值解決
現(xiàn)在,我們嘗試用一個(gè) canceled
布爾值解決:
const App = () => { const [data, setData] = useState(''); const [loading, setLoading] = useState(false); const [person, setPerson] = useState(null); useEffect(() => { let canceled = false; setLoading(true); fakeFetch(person).then(data => { if (!canceled) { setData(data); setLoading(false); } }); return () => (canceled = true); }, [person]); return ( <Fragment> <button onClick={() => setPerson('Nick')}>Nick's Profile</button> <button onClick={() => setPerson('Deb')}>Deb's Profile</button> <button onClick={() => setPerson('Joe')}>Joe's Profile</button> {person && ( <Fragment> <h1>{person}</h1> <p>{loading ? 'Loading...' : data}</p> </Fragment> )} </Fragment> ); };
實(shí)現(xiàn)效果如下:
即便接口沒有按照順序返回,依然不影響最終顯示的數(shù)據(jù)。
useRequest 解決
我們也可以借助 ahooks 的 useRequest
方法,修改后的代碼如下:
const App2 = () => { const [person, setPerson] = useState('Nick'); const { data, loading} = useRequest(() => fakeFetch(person), { refreshDeps: [person], }); const handleClick = (name) => () => { setPerson(name) } return ( <Fragment> <button onClick={handleClick('Nick')}>Nick's Profile</button> <button onClick={handleClick('Deb')}>Deb's Profile</button> <button onClick={() => setPerson('Joe')}>Joe's Profile</button> {person && ( <Fragment> <h1>{person}</h1> <p>{loading ? 'Loading...' : data}</p> </Fragment> )} </Fragment> ); };
代碼效果如上,就不重復(fù)錄制了。
考慮到部分同學(xué)可能會(huì)對(duì) useRequest
的使用感到困惑,我們簡(jiǎn)單介紹一下 useRequest
的使用:
useRequest
的第一個(gè)參數(shù)是一個(gè)異步函數(shù),在組件初次加載時(shí),會(huì)自動(dòng)觸發(fā)該函數(shù)執(zhí)行。同時(shí)自動(dòng)管理該異步函數(shù)的 loading
、 data
、 error
等狀態(tài)。
useRequest
同樣提供了一個(gè) options.refreshDeps
參數(shù),當(dāng)它的值變化后,會(huì)重新觸發(fā)請(qǐng)求。
const [userId, setUserId] = useState('1'); const { data, run } = useRequest(() => getUserSchool(userId), { refreshDeps: [userId], });
上面的示例代碼,useRequest 會(huì)在初始化和 userId 變化時(shí),觸發(fā)函數(shù)執(zhí)行。與下面代碼實(shí)現(xiàn)功能完全一致:
const [userId, setUserId] = useState('1'); const { data, refresh } = useRequest(() => getUserSchool(userId)); useEffect(() => { refresh(); }, [userId]);
Suspense
這篇之所以講 Race Condition,主要還是為了引入講解 Suspense,借助 Suspense,我們同樣可以解決 Race Condition:
// 實(shí)現(xiàn)參考的 React 官方示例:https://codesandbox.io/s/infallible-feather-xjtbu function wrapPromise(promise) { let status = "pending"; let result; let suspender = promise.then( r => { status = "success"; result = r; }, e => { status = "error"; result = e; } ); return { read() { if (status === "pending") { throw suspender; } else if (status === "error") { throw result; } else if (status === "success") { return result; } } }; } const fakeFetch = person => { return new Promise(res => { setTimeout(() => res(`${person}'s data`), Math.random() * 5000); }); }; function fetchData(userId) { return wrapPromise(fakeFetch(userId)) } const initialResource = fetchData('Nick'); function User({ resource }) { const data = resource.read(); return <p>{ data }</p> } const App = () => { const [person, setPerson] = useState('Nick'); const [resource, setResource] = useState(initialResource); const handleClick = (name) => () => { setPerson(name) setResource(fetchData(name)); } return ( <Fragment> <button onClick={handleClick('Nick')}>Nick's Profile</button> <button onClick={handleClick('Deb')}>Deb's Profile</button> <button onClick={handleClick('Joe')}>Joe's Profile</button> <Fragment> <h1>{person}</h1> <Suspense fallback={'loading'}> <User resource={resource} /> </Suspense> </Fragment> </Fragment> ); };
以上就是React競(jìng)態(tài)條件Race Condition實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于React競(jìng)態(tài)條件Race Condition的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react無(wú)限滾動(dòng)組件的實(shí)現(xiàn)示例
本文主要介紹了react無(wú)限滾動(dòng)組件的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05React如何使用create-react-app創(chuàng)建react項(xiàng)目
這篇文章主要介紹了React如何使用create-react-app創(chuàng)建react項(xiàng)目問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03React+Antd+Redux實(shí)現(xiàn)待辦事件的方法
這篇文章主要介紹了React+Antd+Redux實(shí)現(xiàn)待辦事件的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03react實(shí)現(xiàn)簡(jiǎn)單的拖拽功能
這篇文章主要為大家詳細(xì)介紹了react實(shí)現(xiàn)簡(jiǎn)單的拖拽功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03