React?之?Suspense提出的背景及使用詳解
Suspense 提出的背景
假設(shè)我們現(xiàn)在有如下一個(gè)應(yīng)用:
const Articles = () => { const [articles, setArticles] = useState(null) useEffect(() => { getArticles().then((a) => setArticles(a)) }, []) if (articles === null) { return <p>Loading articles...</p> } return ( <ul> {articles.map((article) => ( <li key={article.id}> <h4>{article.title}</h4> <p>{article.abstract}</p> </li> ))} </ul> ) } export default function Profile() { const [user, setUser] = useState(null) useEffect(() => { getUser().then((u) => setUser(u)) }, []) if (user === null) { return <p>Loading user...</p> } return ( <> <h3>{user.name}</h3> <Articles articles={articles} /> </> ) }
該應(yīng)用是一個(gè)用戶的個(gè)人主頁,包含用戶的基本信息(例子中只有名字)以及用戶的文章列表,并且規(guī)定了必須等待用戶獲取成功后才能渲染其基本信息以及文章列表。 該應(yīng)用看似簡單,但卻存在著以下幾個(gè)問題:
- "Waterfalls",意思是文章列表必須要等到用戶請求成功以后才能開始渲染,從而對于文章列表的請求也會(huì)被用戶阻塞,但其實(shí)對于文章的請求是可以同用戶并行的。
- "fetch-on-render",無論是
Profile
還是Articles
組件,都是需要等到渲染一次后才能發(fā)出請求。
對于第一個(gè)問題,我們可以通過修改代碼來優(yōu)化:
const Articles = ({articles}) => { if (articles === null) { return <p>Loading articles...</p> } return ( <ul> {articles.map((article) => ( <li key={article.id}> <h4>{article.title}</h4> <p>{article.abstract}</p> </li> ))} </ul> ) } export default function Profile() { const [user, setUser] = useState(null) const [articles, setArticles] = useState(null) useEffect(() => { getUser().then((u) => setUser(u)) getArticles().then((a) => setArticles(a)) }, []) if (user === null) { return <p>Loading user...</p> } return ( <> <h3>{user.name}</h3> <Articles articles={articles} /> </> ) }
現(xiàn)在獲取用戶和獲取文章列表的邏輯已經(jīng)可以并行了,但是這樣又導(dǎo)致 Articles
組件同其數(shù)據(jù)獲取相關(guān)的邏輯分離,隨著應(yīng)用變得復(fù)雜后,這種方式可能會(huì)難以維護(hù)。同時(shí)第二個(gè)問題 "fetch-on-render" 還是沒有解決。而 Suspense 的出現(xiàn)可以很好的解決這些問題,接下來就來看看是如何解決的。
Suspense 的使用
Suspense 用于數(shù)據(jù)獲取
還是上面的例子,我們使用 Suspense 來改造一下:
// Profile.js import React, {Suspense} from 'react' import User from './User' import Articles from './Articles' export default function Profile() { return ( <Suspense fallback={<p>Loading user...</p>}> <User /> <Suspense fallback={<p>Loading articles...</p>}> <Articles /> </Suspense> </Suspense> ) } // Articles.js import React from 'react' import {getArticlesResource} from './resource' const articlesResource = getArticlesResource() const Articles = () => { debugger const articles = articlesResource.read() return ( <ul> {articles.map((article) => ( <li key={article.id}> <h4>{article.title}</h4> <p>{article.abstract}</p> </li> ))} </ul> ) } // User.js import React from 'react' import {getUserResource} from './resource' const userResource = getUserResource() const User = () => { const user = userResource.read() return <h3>{user.name}</h3> } // resource.js export function wrapPromise(promise) { let status = 'pending' let result let suspender = promise.then( (r) => { debugger 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 } }, } } export function getArticles() { return new Promise((resolve, reject) => { const list = [...new Array(10)].map((_, index) => ({ id: index, title: `Title${index + 1}`, abstract: `Abstract${index + 1}`, })) setTimeout(() => { resolve(list) }, 2000) }) } export function getUser() { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ name: 'Ayou', age: 18, vocation: 'Program Ape', }) }, 3000) }) } export const getUserResource = () => { return wrapPromise(getUser()) } export const getArticlesResource = () => { return wrapPromise(getArticles()) }
首先,在 Profile.js
中開始引入 User
和 Articles
的時(shí)候就已經(jīng)開始請求數(shù)據(jù)了,即 "Render-as-You-Fetch"(渲染的時(shí)候請求),且兩者是并行的。當(dāng)渲染到 User
組件的時(shí)候,由于此時(shí)接口請求還未返回,const user = userResource.read()
會(huì)拋出異常:
... read() { if (status === 'pending') { throw suspender } else if (status === 'error') { throw result } else if (status === 'success') { return result } }, ...
而 Suspense
組件的作用是,當(dāng)發(fā)現(xiàn)其包裹的組件拋出異常且異常為 Promise
對象時(shí),會(huì)渲染 fallback
中的內(nèi)容,即 <p>Loading user...</p>
。等到 Promise
對象 resolve
的時(shí)候會(huì)再次觸發(fā)重新渲染,顯示其包裹的內(nèi)容,又因?yàn)楂@取文章列表的時(shí)間比用戶短,所以這里會(huì)同時(shí)顯示用戶信息及其文章列表(具體過程后續(xù)會(huì)再進(jìn)行分析)。這樣,通過 Suspense
組件,我們就解決了前面的兩個(gè)問題。
同時(shí),使用 Suspense
還會(huì)有另外一個(gè)好處,假設(shè)我們現(xiàn)在改變我們的需求,允許用戶信息和文章列表獨(dú)立渲染,則使用 Suspense
重構(gòu)起來會(huì)比較簡單:
而如果使用原來的方式,則需要修改的地方比較多:
可見,使用 Suspense
會(huì)帶來很多好處。當(dāng)然,上文為了方便說明,寫得非常簡單,實(shí)際開發(fā)時(shí)會(huì)結(jié)合 Relay 這樣的庫來使用,由于這一款目前還處于試驗(yàn)階段,所以暫時(shí)先不做過多的討論。
Suspense
除了可以用于上面的數(shù)據(jù)獲取這種場景外,還可以用來實(shí)現(xiàn) Lazy Component
。
Lazy Component
import React, {Suspense} from 'react' const MyComp = React.lazy(() => import('./MyComp')) export default App() { return ( <Suspense fallback={<p>Loading Component...</p>}> <MyComp /> </Suspense> ) }
我們知道 import('./MyComp')
返回的是一個(gè) Promise
對象,其 resolve
的是一個(gè)模塊,既然如此那這樣也是可以的:
import React, {Suspense} from 'react' const MyComp = React.lazy( () => new Promise((resolve) => setTimeout( () => resolve({ default: function MyComp() { return <div>My Comp</div> }, }), 1000 ) ) ) export default function App() { return ( <Suspense fallback={<p>Loading Component...</p>}> <MyComp /> </Suspense> ) }
甚至,我們可以通過請求來獲取 Lazy Component
的代碼:
import React, {Suspense} from 'react' const MyComp = React.lazy( () => new Promise(async (resolve) => { const code = await fetch('http://xxxx') const module = {exports: {}} Function('export, module', code)(module.exports, module) resolve({default: module.exports}) }) ) export default function App() { return ( <Suspense fallback={<p>Loading Component...</p>}> <MyComp /> </Suspense> ) }
這也是我們實(shí)現(xiàn)遠(yuǎn)程組件的基本原理。
原理
介紹了這么多關(guān)于 Suspense
的內(nèi)容后,你一定很好奇它到底是如何實(shí)現(xiàn)的吧,我們先不研究 React 源碼,先嘗試自己實(shí)現(xiàn)一個(gè) Suspense
:
import React, {Component} from 'react' export default class Suspense extends Component { state = { isLoading: false, } componentDidCatch(error, info) { if (this._mounted) { if (typeof error.then === 'function') { this.setState({isLoading: true}) error.then(() => { if (this._mounted) { this.setState({isLoading: false}) } }) } } } componentDidMount() { this._mounted = true } componentWillUnmount() { this._mounted = false } render() { const {children, fallback} = this.props const {isLoading} = this.state return isLoading ? fallback : children } }
其核心原理就是利用了 “Error Boundary” 來捕獲子組件中的拋出的異常,且如果拋出的異常為 Promise
對象,則在傳入其 then
方法的回調(diào)中改變 state
觸發(fā)重新渲染。
接下來,我們還是用上面的例子來分析一下整個(gè)過程:
export default function Profile() { return ( <Suspense fallback={<p>Loading user...</p>}> <User /> <Suspense fallback={<p>Loading articles...</p>}> <Articles /> </Suspense> </Suspense> ) }
我們知道 React 在渲染時(shí)會(huì)構(gòu)建 Fiber Tree,當(dāng)處理到 User
組件時(shí),React 代碼中會(huì)捕獲到異常:
do { try { workLoopConcurrent() break } catch (thrownValue) { handleError(root, thrownValue) } } while (true)
其中,異常處理函數(shù) handleError
主要做兩件事:
throwException( root, erroredWork.return, erroredWork, thrownValue, workInProgressRootRenderLanes ) completeUnitOfWork(erroredWork)
其中,throwException
主要是往上找到最近的 Suspense
類型的 Fiber,并更新其 updateQueue
:
const wakeables: Set<Wakeable> = (workInProgress.updateQueue: any) if (wakeables === null) { const updateQueue = (new Set(): any) updateQueue.add(wakeable) // wakeable 是 handleError(root, thrownValue) 中的 thrownValue,是一個(gè) Promise 對象 workInProgress.updateQueue = updateQueue } else { wakeables.add(wakeable) }
而 completeUnitOfWork(erroredWork)
在React 源碼解讀之首次渲染流程中已經(jīng)介紹過了,此處就不再贅述了。
render
階段后,會(huì)形成如下所示的 Fiber 結(jié)構(gòu):
之后會(huì)進(jìn)入 commit
階段,將 Fiber 對應(yīng)的 DOM 插入到容器之中:
注意到 Loading articles...
雖然也被插入了,但確是不可見的。
前面提到過 Suspense
的 updateQueue
中保存了 Promise
請求對象,我們需要在其 resolve
以后觸發(fā)應(yīng)用的重新渲染,這一步驟仍然是在 commit
階段實(shí)現(xiàn)的:
function commitWork(current: Fiber | null, finishedWork: Fiber): void { ... case SuspenseComponent: { commitSuspenseComponent(finishedWork); attachSuspenseRetryListeners(finishedWork); return; } ... }
function attachSuspenseRetryListeners(finishedWork: Fiber) { // If this boundary just timed out, then it will have a set of wakeables. // For each wakeable, attach a listener so that when it resolves, React // attempts to re-render the boundary in the primary (pre-timeout) state. const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any) if (wakeables !== null) { finishedWork.updateQueue = null let retryCache = finishedWork.stateNode if (retryCache === null) { retryCache = finishedWork.stateNode = new PossiblyWeakSet() } wakeables.forEach((wakeable) => { // Memoize using the boundary fiber to prevent redundant listeners. let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable) if (!retryCache.has(wakeable)) { if (enableSchedulerTracing) { if (wakeable.__reactDoNotTraceInteractions !== true) { retry = Schedule_tracing_wrap(retry) } } retryCache.add(wakeable) // promise resolve 了以后觸發(fā) react 的重新渲染 wakeable.then(retry, retry) } }) } }
總結(jié)
本文介紹了 Suspense
提出的背景、使用方式以及原理,從文中可看出 Suspense
用于數(shù)據(jù)獲取對我們的開發(fā)方式將是一個(gè)巨大的影響,但是目前還處在實(shí)驗(yàn)階段,所以留給“中國隊(duì)”的時(shí)間還是很充足的。
以上就是React 之 Suspense提出的背景及使用詳解的詳細(xì)內(nèi)容,更多關(guān)于React Suspense使用背景的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React項(xiàng)目build打包頁面空白的解決方案
React項(xiàng)目執(zhí)行build命令后,在本地服務(wù)器打開頁面是空白的,本文主要介紹了React項(xiàng)目build打包頁面空白的解決方案,感興趣的可以了解一下2023-08-08每天一個(gè)hooks學(xué)習(xí)之useUnmount
這篇文章主要為大家介紹了每天一個(gè)hooks學(xué)習(xí)之useUnmount,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05通過實(shí)例學(xué)習(xí)React中事件節(jié)流防抖
這篇文章主要介紹了通過實(shí)例學(xué)習(xí)React中事件節(jié)流防抖,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06React-Native中禁用Navigator手勢返回的示例代碼
本篇文章主要介紹了React-Native中禁用Navigator手勢返回的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-09-09深入理解react-router 路由的實(shí)現(xiàn)原理
這篇文章主要介紹了深入理解react-router 路由的實(shí)現(xiàn)原理,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09