網站首頁 編程語言 正文
競態條件
Race Condition,中文譯為競態條件,旨在描述一個系統或者進程的輸出,依賴于不受控制事件的出現順序或者出現時機。
舉個簡單的例子:
if (x == 5) // The "Check" { y = x * 2; // The "Act" // 如果其他的線程在 "if (x == 5)" and "y = x * 2" 執行之間更改了 x 的值 // y 就可能不等于 10. }
你可能想,JavaScript 是單線程,怎么可能出現這個問題?
React 與競態條件
確實如此,但前端有異步渲染,所以競態條件依然有可能出現,我們舉個 React 中常見的例子。
這是一個非常典型的數據獲取代碼:
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 }); } // ... }
看起來沒什么問題,但這段代碼還沒有實現數據更新,我們再改一下:
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 }); } // ... }
當組件傳入新的 id
時,我們根據新的 id
請求數據,然后 setState
最新獲取的數據。
這時就可能出現競態條件,比如用戶選完立刻點擊下一頁,我們請求 id
為 1 的數據,緊接著請求 id
為 2 的數據,但因為網絡或者接口處理等原因,id
為 2 的接口提前返回,便會先展示 id
為 2 的數據,再展示 id
為 1 的數據,這就導致了錯誤。
我們可以想想遇到這種問題的場景,比如類似于百度的搜索功能,切換 tab 等場景,雖然我們也可以使用諸如 debounce 的方式來緩解,但效果還是會差點,比如使用 debounce,用戶在輸入搜索詞的時候,展示內容會長期處于空白狀態,對于用戶體驗而言,我們可以做的更好。
那么我們該如何解決呢?一種是在切換的時候取消請求,還有一種是借助一個布爾值來判斷是否需要更新,比如這樣:
function Article({ id }) { const [article, setArticle] = useState(null); useEffect(() => { let didCancel = false; async function fetchData() { const article = await API.fetchArticle(id); // 如果 didCancel 為 true 說明用戶已經取消了 if (!didCancel) { setArticle(article); } } fetchData(); // 執行下一個 effect 之前會執行 return () => { didCancel = true; }; }, [id]); // ... }
當然你也可以用 ahooks 中的 useRequest
,它的內部有一個 ref 變量記錄最新的 promise,也可以解決 Race Condition 的問題:
function Article({ id }) { const { data, loading, error} = useRequest(() => fetchArticle(id), { refreshDeps: [id] }); // ... }
效果演示
問題復現
為了方便大家自己測試這個問題,我們提供相對完整的代碼。以 《Avoiding Race Conditions when Fetching Data with React Hooks》中的例子為例,出現 Race Condition 問題的代碼如下:
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> ); };
我們實現了一個 fakeFetch
函數,用于模擬接口的返回,具體返回的時間為 Math.random() * 5000)
,用于模擬數據的隨機返回。
實現效果如下:
從效果圖中可以看到,我們按順序點擊了 Nick
、Deb
、Joe
,理想情況下,結果應該顯示 Joe's Data
,但最終顯示的數據為最后返回的 Nick's Data
。
布爾值解決
現在,我們嘗試用一個 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> ); };
實現效果如下:
即便接口沒有按照順序返回,依然不影響最終顯示的數據。
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> ); };
代碼效果如上,就不重復錄制了。
考慮到部分同學可能會對 useRequest
的使用感到困惑,我們簡單介紹一下 useRequest
的使用:
useRequest
的第一個參數是一個異步函數,在組件初次加載時,會自動觸發該函數執行。同時自動管理該異步函數的 loading
、 data
、 error
等狀態。
useRequest
同樣提供了一個 options.refreshDeps
參數,當它的值變化后,會重新觸發請求。
const [userId, setUserId] = useState('1'); const { data, run } = useRequest(() => getUserSchool(userId), { refreshDeps: [userId], });
上面的示例代碼,useRequest 會在初始化和 userId 變化時,觸發函數執行。與下面代碼實現功能完全一致:
const [userId, setUserId] = useState('1'); const { data, refresh } = useRequest(() => getUserSchool(userId)); useEffect(() => { refresh(); }, [userId]);
Suspense
這篇之所以講 Race Condition,主要還是為了引入講解 Suspense,借助 Suspense,我們同樣可以解決 Race Condition:
// 實現參考的 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> ); };
原文鏈接:https://juejin.cn/post/7163202327594139679
相關推薦
- 2022-03-20 Android實現桌面快捷方式實例代碼_Android
- 2022-11-27 Python?OpenCV實現圖像增強操作詳解_python
- 2023-04-18 利用Python操作MongoDB數據庫的詳細指南_python
- 2024-03-09 【Redis】什么是緩存穿透,如何預防緩存穿透?
- 2022-06-08 FreeRTOS實時操作系統的任務概要講解_操作系統
- 2022-10-16 python正則表達式re.group()用法_python
- 2022-02-09 C++學習之IO流(輸入輸出流)詳解_C 語言
- 2023-10-11 lambda Collectors類的靜態工廠方法
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支