React?Suspense解決競態(tài)條件詳解
前言
在上一篇《React 之 Race Condition》中,我們最后引入了 Suspense 來解決競態(tài)條件問題,本篇我們來詳細(xì)講解一下 Suspense。
Suspense
React 16.6 新增了 <Suspense>
組件,讓你可以“等待”目標(biāo)代碼加載,并且可以直接指定一個加載的界面(像是個 spinner),讓它在用戶等待的時候顯示。
目前,Suspense 僅支持的使用場景是:通過 React.lazy
動態(tài)加載組件
const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懶加載 // 在 ProfilePage 組件處于加載階段時顯示一個 spinner <Suspense fallback={<Spinner />}> <ProfilePage /> </Suspense>
執(zhí)行機(jī)制
但這并不意味著 Suspense 不可以單獨使用,我們可以寫個 Suspense 單獨使用的例子,不過目前使用起來會有些麻煩,但相信 React 官方會持續(xù)優(yōu)化這個 API。
let data, promise; function fetchData() { if (data) return data; promise = new Promise(resolve => { setTimeout(() => { data = 'data fetched' resolve() }, 3000) }) throw promise; } function Content() { const data = fetchData(); return <p>{data}</p> } function App() { return ( <Suspense fallback={'loading data'}> <Content /> </Suspense> ) }
這是一個非常簡單的使用示例,但卻可以用來解釋 Suspense 的執(zhí)行機(jī)制。
最一開始 <Content>
組件會 throw 一個 promise,React 會捕獲這個異常,發(fā)現(xiàn)是 promise 后,會在這個 promise 上追加一個 then 函數(shù),在 then 函數(shù)中執(zhí)行 Suspense 組件的更新,然后展示 fallback 內(nèi)容。
等 fetchData 中的 promise resolve 后,會執(zhí)行追加的 then 函數(shù),觸發(fā) Suspense 組件的更新,此時有了 data 數(shù)據(jù),因為沒有異常,React 會刪除 fallback 組件,正常展示 <Content />
組件。
實際應(yīng)用
如果我們每個請求都這樣去寫,代碼會很冗余,雖然有 react-cache
這個 npm 包,但上次更新已經(jīng)是 4 年之前了,不過通過查看包源碼以及參考 React 官方的示例代碼,在實際項目中,我們可以這樣去寫:
// 1. 通用的 wrapPromise 函數(shù) 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 = () => { return new Promise(res => { setTimeout(() => res('data fetched'), 3000); }); }; // 2. 在渲染前發(fā)起請求 const resource = wrapPromise(fakeFetch()); function Content() { // 3. 通過 resource.read() 獲取接口返回結(jié)果 const data = resource.read(); return <p>{data}</p> } function App() { return ( <Suspense fallback={'loading data'}> <Content /> </Suspense> ) }
在這段代碼里,我們聲明了一個 wrapPromise
函數(shù),它接收一個 promise,比如 fetch 請求。函數(shù)返回一個帶有 read 方法的對象,這是因為封裝成方法后,代碼可以延遲執(zhí)行,我們就可以在 Suspense 組件更新的時候再執(zhí)行方法,從而獲取最新的返回結(jié)果。
函數(shù)內(nèi)部記錄了三種狀態(tài),pending
、success
、error
,根據(jù)狀態(tài)返回不同的內(nèi)容。
你可能會想,如果我們還要根據(jù) id 之類的數(shù)據(jù)點擊請求數(shù)據(jù)呢?使用 Suspense 該怎么做呢?React 官方文檔也給了示例代碼:
const fakeFetch = (id) => { return new Promise(res => { setTimeout(() => res(`${id} data fetched`), 3000); }); }; // 1. 依然是直接請求數(shù)據(jù) const initialResource = wrapPromise(fakeFetch(1)); function Content({resource}) { // 3. 通過 resource.read() 獲取接口返回結(jié)果 const data = resource.read(); return <p>{data}</p> } function App() { // 2. 將 wrapPromise 返回的對象作為 props 傳遞給組件 const [resource, setResource] = useState(initialResource); // 4. 重新請求 const handleClick = (id) => () => { setResource(wrapPromise(fakeFetch(id))); } return ( <Fragment> <button onClick={handleClick(1)}>tab 1</button> <button onClick={handleClick(2)}>tab 2</button> <Suspense fallback={'loading data'}> <Content resource={resource} /> </Suspense> </Fragment> ) }
好處:請求前置
使用 Suspense 一個非常大的好處就是請求是一開始就執(zhí)行的?;叵脒^往的發(fā)送請求的時機(jī),我們都是在 compentDidMount 的時候再請求的,React 是先渲染的節(jié)點再發(fā)送的請求,然而使用 Suspense,我們是先發(fā)送請求再渲染的節(jié)點,這就帶來了體驗上的提升。
尤其當(dāng)請求多個接口的時候,借助 Suspense,我們可以實現(xiàn)接口并行處理以及提早展現(xiàn),舉個例子:
function fetchData(id) { return { user: wrapPromise(fakeFetchUser(id)), posts: wrapPromise(fakeFetchPosts(id)) }; } const fakeFetchUser = (id) => { return new Promise(res => { setTimeout(() => res(`user ${id} data fetched`), 5000 * Math.random()); }); }; const fakeFetchPosts = (id) => { return new Promise(res => { setTimeout(() => res(`posts ${id} data fetched`), 5000 * Math.random()); }); }; const initialResource = fetchData(1); function User({resource}) { const data = resource.user.read(); return <p>{data}</p> } function Posts({resource}) { const data = resource.posts.read(); return <p>{data}</p> } function App() { const [resource, setResource] = useState(initialResource); const handleClick = (id) => () => { setResource(fetchData(id)); } return ( <Fragment> <p><button onClick={handleClick(Math.ceil(Math.random() * 10))}>next user</button></p> <Suspense fallback={'loading user'}> <User resource={resource} /> <Suspense fallback={'loading posts'}> <Posts resource={resource} /> </Suspense> </Suspense> </Fragment> ) }
在這個示例代碼中,user 和 posts 接口是并行請求的,如果 posts 接口提前返回,而 user 接口還未返回,會等到 user 接口返回后,再一起展現(xiàn),但如果 user 接口提前返回,posts 接口后返回,則會先展示 user 信息,然后顯示 loading posts,等 posts 接口返回,再展示 posts 內(nèi)容。
這聽起來好像沒什么,但是想想如果我們是以前會怎么做,我們可能會用一個 Promise.all 來實現(xiàn),但是 Promise.all 的問題就在于必須等待所有接口返回才會執(zhí)行,而且如果其中有一個 reject 了,都會走向 catch 邏輯。使用 Suspense,我們可以做到更好的展示效果。
好處:解決競態(tài)條件
使用 Suspense 可以有效的解決 Race Conditions(競態(tài)條件) 的問題,關(guān)于 Race Conditions 可以參考《React 之 Race Condition》。
Suspense 之所以能夠有效的解決 Race Conditions 問題,就在于傳統(tǒng)的實現(xiàn)中,我們需要考慮 setState 的正確時機(jī),執(zhí)行順序是:1. 請求數(shù)據(jù) 2. 數(shù)據(jù)返回 3. setState 數(shù)據(jù)
而在 Suspense 中,我們請求后,立刻就設(shè)置了 setState,然后就只用等待請求返回,React 執(zhí)行 Suspense 的再次更新就好了,執(zhí)行順序是:1. 請求數(shù)據(jù) 2. setState 數(shù)據(jù) 3. 數(shù)據(jù)返回 4. Suspense 重新渲染,所以大大降低了出錯的概率。
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> ); };
錯誤處理
注意我們使用的 wrapPromise 函數(shù):
function wrapPromise(promise) { // ... return { read() { if (status === "pending") { throw suspender; } else if (status === "error") { throw result; } else if (status === "success") { return result; } } }; }
當(dāng) status 為 error 的時候,會 throw result 出來,如果 throw 是一個 promise,React 可以處理,但如果只是一個 error,React 就處理不了了,這就會導(dǎo)致渲染出現(xiàn)問題,所以我們有必要針對 status 為 error 的情況進(jìn)行處理,React 官方文檔也提供了方法,那就是定義一個錯誤邊界組件:
// 定義一個錯誤邊界組件 class ErrorBoundary extends React.Component { state = { hasError: false, error: null }; static getDerivedStateFromError(error) { return { hasError: true, error }; } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } function App() { // ... return ( <Fragment> <button onClick={handleClick(1)}>tab 1</button> <button onClick={handleClick(2)}>tab 2</button> <ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}> <Suspense fallback={'loading data'}> <Content resource={resource} /> </Suspense> </ErrorBoundary> </Fragment> ) }
當(dāng) <Content />
組件 throw 出 error 的時候,就會被 <ErrorBoundary />
組件捕獲,然后展示 fallback 的內(nèi)容。
源碼
那 Suspense 的源碼呢?我們查看 React.js 的源碼:
import { REACT_SUSPENSE_TYPE } from 'shared/ReactSymbols'; export { REACT_SUSPENSE_TYPE as Suspense };
再看下shared/ReactSymbols
的源碼:
export const REACT_SUSPENSE_TYPE: symbol = Symbol.for('react.suspense');
所以當(dāng)我們寫一個 Suspense 組件的時候:
<Suspense fallback={'loading data'}> <Content /> </Suspense> // 被轉(zhuǎn)譯為 React.createElement(Suspense, { fallback: 'loading data' }, React.createElement(Content, null));
createElement 傳入的 Suspense 就只是一個常量而已,具體的處理邏輯會在以后的文章中慢慢講解。
React 系列
- React 之 createElement 源碼解讀
- React 之元素與組件的區(qū)別
- React 之 Refs 的使用和 forwardRef 的源碼解讀
- React 之 Context 的變遷與背后實現(xiàn)
- React 之 Race Condition
以上就是React Suspense解決競態(tài)條件詳解的詳細(xì)內(nèi)容,更多關(guān)于React Suspense競態(tài)條件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于antd tree和父子組件之間的傳值問題(react 總結(jié))
這篇文章主要介紹了關(guān)于antd tree 和父子組件之間的傳值問題,是小編給大家總結(jié)的一些react知識點,本文通過一個項目需求實例代碼詳解給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-06-06