React?之?Suspense提出的背景及使用詳解
Suspense 提出的背景
假設(shè)我們現(xiàn)在有如下一個應(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)用是一個用戶的個人主頁,包含用戶的基本信息(例子中只有名字)以及用戶的文章列表,并且規(guī)定了必須等待用戶獲取成功后才能渲染其基本信息以及文章列表。 該應(yīng)用看似簡單,但卻存在著以下幾個問題:
- "Waterfalls",意思是文章列表必須要等到用戶請求成功以后才能開始渲染,從而對于文章列表的請求也會被用戶阻塞,但其實對于文章的請求是可以同用戶并行的。
- "fetch-on-render",無論是
Profile還是Articles組件,都是需要等到渲染一次后才能發(fā)出請求。
對于第一個問題,我們可以通過修改代碼來優(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ù)雜后,這種方式可能會難以維護(hù)。同時第二個問題 "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 的時候就已經(jīng)開始請求數(shù)據(jù)了,即 "Render-as-You-Fetch"(渲染的時候請求),且兩者是并行的。當(dāng)渲染到 User 組件的時候,由于此時接口請求還未返回,const user = userResource.read() 會拋出異常:
...
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 對象時,會渲染 fallback 中的內(nèi)容,即 <p>Loading user...</p>。等到 Promise 對象 resolve 的時候會再次觸發(fā)重新渲染,顯示其包裹的內(nèi)容,又因為獲取文章列表的時間比用戶短,所以這里會同時顯示用戶信息及其文章列表(具體過程后續(xù)會再進(jìn)行分析)。這樣,通過 Suspense 組件,我們就解決了前面的兩個問題。
同時,使用 Suspense 還會有另外一個好處,假設(shè)我們現(xiàn)在改變我們的需求,允許用戶信息和文章列表獨立渲染,則使用 Suspense 重構(gòu)起來會比較簡單:

而如果使用原來的方式,則需要修改的地方比較多:

可見,使用 Suspense 會帶來很多好處。當(dāng)然,上文為了方便說明,寫得非常簡單,實際開發(fā)時會結(jié)合 Relay 這樣的庫來使用,由于這一款目前還處于試驗階段,所以暫時先不做過多的討論。
Suspense 除了可以用于上面的數(shù)據(jù)獲取這種場景外,還可以用來實現(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') 返回的是一個 Promise 對象,其 resolve 的是一個模塊,既然如此那這樣也是可以的:
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>
)
}
這也是我們實現(xiàn)遠(yuǎn)程組件的基本原理。
原理
介紹了這么多關(guān)于 Suspense 的內(nèi)容后,你一定很好奇它到底是如何實現(xiàn)的吧,我們先不研究 React 源碼,先嘗試自己實現(xiàn)一個 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ā)重新渲染。
接下來,我們還是用上面的例子來分析一下整個過程:
export default function Profile() {
return (
<Suspense fallback={<p>Loading user...</p>}>
<User />
<Suspense fallback={<p>Loading articles...</p>}>
<Articles />
</Suspense>
</Suspense>
)
}
我們知道 React 在渲染時會構(gòu)建 Fiber Tree,當(dāng)處理到 User 組件時,React 代碼中會捕獲到異常:
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,是一個 Promise 對象
workInProgress.updateQueue = updateQueue
} else {
wakeables.add(wakeable)
}

而 completeUnitOfWork(erroredWork) 在React 源碼解讀之首次渲染流程中已經(jīng)介紹過了,此處就不再贅述了。
render 階段后,會形成如下所示的 Fiber 結(jié)構(gòu):

之后會進(jìn)入 commit 階段,將 Fiber 對應(yīng)的 DOM 插入到容器之中:

注意到 Loading articles... 雖然也被插入了,但確是不可見的。
前面提到過 Suspense 的 updateQueue 中保存了 Promise 請求對象,我們需要在其 resolve 以后觸發(fā)應(yīng)用的重新渲染,這一步驟仍然是在 commit 階段實現(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ā)方式將是一個巨大的影響,但是目前還處在實驗階段,所以留給“中國隊”的時間還是很充足的。
以上就是React 之 Suspense提出的背景及使用詳解的詳細(xì)內(nèi)容,更多關(guān)于React Suspense使用背景的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
每天一個hooks學(xué)習(xí)之useUnmount
這篇文章主要為大家介紹了每天一個hooks學(xué)習(xí)之useUnmount,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
通過實例學(xué)習(xí)React中事件節(jié)流防抖
這篇文章主要介紹了通過實例學(xué)習(xí)React中事件節(jié)流防抖,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,,需要的朋友可以參考下2019-06-06
React-Native中禁用Navigator手勢返回的示例代碼
本篇文章主要介紹了React-Native中禁用Navigator手勢返回的示例代碼,具有一定的參考價值,有興趣的可以了解一下2017-09-09
深入理解react-router 路由的實現(xiàn)原理
這篇文章主要介紹了深入理解react-router 路由的實現(xiàn)原理,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09

