React?Suspense解決競態(tài)條件詳解
前言
在上一篇《React 之 Race Condition》中,我們最后引入了 Suspense 來解決競態(tài)條件問題,本篇我們來詳細(xì)講解一下 Suspense。
Suspense
React 16.6 新增了 <Suspense> 組件,讓你可以“等待”目標(biāo)代碼加載,并且可以直接指定一個(gè)加載的界面(像是個(gè) spinner),讓它在用戶等待的時(shí)候顯示。
目前,Suspense 僅支持的使用場景是:通過 React.lazy 動(dòng)態(tài)加載組件
const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懶加載
// 在 ProfilePage 組件處于加載階段時(shí)顯示一個(gè) spinner
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
執(zhí)行機(jī)制
但這并不意味著 Suspense 不可以單獨(dú)使用,我們可以寫個(gè) Suspense 單獨(dú)使用的例子,不過目前使用起來會(huì)有些麻煩,但相信 React 官方會(huì)持續(xù)優(yōu)化這個(gè) 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>
)
}
這是一個(gè)非常簡單的使用示例,但卻可以用來解釋 Suspense 的執(zhí)行機(jī)制。
最一開始 <Content> 組件會(huì) throw 一個(gè) promise,React 會(huì)捕獲這個(gè)異常,發(fā)現(xiàn)是 promise 后,會(huì)在這個(gè) promise 上追加一個(gè) then 函數(shù),在 then 函數(shù)中執(zhí)行 Suspense 組件的更新,然后展示 fallback 內(nèi)容。
等 fetchData 中的 promise resolve 后,會(huì)執(zhí)行追加的 then 函數(shù),觸發(fā) Suspense 組件的更新,此時(shí)有了 data 數(shù)據(jù),因?yàn)闆]有異常,React 會(huì)刪除 fallback 組件,正常展示 <Content /> 組件。
實(shí)際應(yīng)用
如果我們每個(gè)請求都這樣去寫,代碼會(huì)很冗余,雖然有 react-cache 這個(gè) npm 包,但上次更新已經(jīng)是 4 年之前了,不過通過查看包源碼以及參考 React 官方的示例代碼,在實(shí)際項(xiàng)目中,我們可以這樣去寫:
// 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>
)
}
在這段代碼里,我們聲明了一個(gè) wrapPromise 函數(shù),它接收一個(gè) promise,比如 fetch 請求。函數(shù)返回一個(gè)帶有 read 方法的對象,這是因?yàn)榉庋b成方法后,代碼可以延遲執(zhí)行,我們就可以在 Suspense 組件更新的時(shí)候再執(zhí)行方法,從而獲取最新的返回結(jié)果。
函數(shù)內(nèi)部記錄了三種狀態(tài),pending、success、error,根據(jù)狀態(tài)返回不同的內(nèi)容。
你可能會(huì)想,如果我們還要根據(jù) id 之類的數(shù)據(jù)點(diǎn)擊請求數(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 一個(gè)非常大的好處就是請求是一開始就執(zhí)行的?;叵脒^往的發(fā)送請求的時(shí)機(jī),我們都是在 compentDidMount 的時(shí)候再請求的,React 是先渲染的節(jié)點(diǎn)再發(fā)送的請求,然而使用 Suspense,我們是先發(fā)送請求再渲染的節(jié)點(diǎn),這就帶來了體驗(yàn)上的提升。
尤其當(dāng)請求多個(gè)接口的時(shí)候,借助 Suspense,我們可以實(shí)現(xiàn)接口并行處理以及提早展現(xiàn),舉個(gè)例子:
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>
)
}
在這個(gè)示例代碼中,user 和 posts 接口是并行請求的,如果 posts 接口提前返回,而 user 接口還未返回,會(huì)等到 user 接口返回后,再一起展現(xiàn),但如果 user 接口提前返回,posts 接口后返回,則會(huì)先展示 user 信息,然后顯示 loading posts,等 posts 接口返回,再展示 posts 內(nèi)容。

這聽起來好像沒什么,但是想想如果我們是以前會(huì)怎么做,我們可能會(huì)用一個(gè) Promise.all 來實(shí)現(xiàn),但是 Promise.all 的問題就在于必須等待所有接口返回才會(huì)執(zhí)行,而且如果其中有一個(gè) reject 了,都會(huì)走向 catch 邏輯。使用 Suspense,我們可以做到更好的展示效果。
好處:解決競態(tài)條件
使用 Suspense 可以有效的解決 Race Conditions(競態(tài)條件) 的問題,關(guān)于 Race Conditions 可以參考《React 之 Race Condition》。
Suspense 之所以能夠有效的解決 Race Conditions 問題,就在于傳統(tǒng)的實(shí)現(xiàn)中,我們需要考慮 setState 的正確時(shí)機(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 重新渲染,所以大大降低了出錯(cuò)的概率。
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>
);
};
錯(cuò)誤處理
注意我們使用的 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 的時(shí)候,會(huì) throw result 出來,如果 throw 是一個(gè) promise,React 可以處理,但如果只是一個(gè) error,React 就處理不了了,這就會(huì)導(dǎo)致渲染出現(xiàn)問題,所以我們有必要針對 status 為 error 的情況進(jìn)行處理,React 官方文檔也提供了方法,那就是定義一個(gè)錯(cuò)誤邊界組件:
// 定義一個(gè)錯(cuò)誤邊界組件
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 的時(shí)候,就會(huì)被 <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)我們寫一個(gè) Suspense 組件的時(shí)候:
<Suspense fallback={'loading data'}>
<Content />
</Suspense>
// 被轉(zhuǎn)譯為
React.createElement(Suspense, {
fallback: 'loading data'
}, React.createElement(Content, null));
createElement 傳入的 Suspense 就只是一個(gè)常量而已,具體的處理邏輯會(huì)在以后的文章中慢慢講解。
React 系列
- React 之 createElement 源碼解讀
- React 之元素與組件的區(qū)別
- React 之 Refs 的使用和 forwardRef 的源碼解讀
- React 之 Context 的變遷與背后實(shí)現(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知識(shí)點(diǎn),本文通過一個(gè)項(xiàng)目需求實(shí)例代碼詳解給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-06-06
React啟動(dòng)時(shí)webpack版本沖突報(bào)錯(cuò)的解決辦法
在啟動(dòng)React應(yīng)用時(shí),遇到Webpack版本不匹配導(dǎo)致的運(yùn)行錯(cuò)誤,解決方法包括刪除全局及局部的webpack和webpack-cli,然后根據(jù)項(xiàng)目需求安裝特定版本的webpack,本文通過代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
react自動(dòng)化構(gòu)建路由的實(shí)現(xiàn)
這篇文章主要介紹了react自動(dòng)化構(gòu)建路由的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04

