您好,登錄后才能下訂單哦!
本篇內容主要講解“React之Suspense提出的背景及使用方法是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“React之Suspense提出的背景及使用方法是什么”吧!
假設我們現在有如下一個應用:
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}> <h5>{article.title}</h5> <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 ( <> <h4>{user.name}</h4> <Articles articles={articles} /> </> ) }
該應用是一個用戶的個人主頁,包含用戶的基本信息(例子中只有名字)以及用戶的文章列表,并且規定了必須等待用戶獲取成功后才能渲染其基本信息以及文章列表。 該應用看似簡單,但卻存在著以下幾個問題:
"Waterfalls",意思是文章列表必須要等到用戶請求成功以后才能開始渲染,從而對于文章列表的請求也會被用戶阻塞,但其實對于文章的請求是可以同用戶并行的。
"fetch-on-render",無論是 Profile
還是 Articles
組件,都是需要等到渲染一次后才能發出請求。
對于第一個問題,我們可以通過修改代碼來優化:
const Articles = ({articles}) => { if (articles === null) { return <p>Loading articles...</p> } return ( <ul> {articles.map((article) => ( <li key={article.id}> <h5>{article.title}</h5> <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 ( <> <h4>{user.name}</h4> <Articles articles={articles} /> </> ) }
現在獲取用戶和獲取文章列表的邏輯已經可以并行了,但是這樣又導致 Articles
組件同其數據獲取相關的邏輯分離,隨著應用變得復雜后,這種方式可能會難以維護。同時第二個問題 "fetch-on-render" 還是沒有解決。而 Suspense 的出現可以很好的解決這些問題,接下來就來看看是如何解決的。
還是上面的例子,我們使用 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}> <h5>{article.title}</h5> <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 <h4>{user.name}</h4> } // 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
的時候就已經開始請求數據了,即 "Render-as-You-Fetch"(渲染的時候請求),且兩者是并行的。當渲染到 User
組件的時候,由于此時接口請求還未返回,const user = userResource.read()
會拋出異常:
... read() { if (status === 'pending') { throw suspender } else if (status === 'error') { throw result } else if (status === 'success') { return result } }, ...
而 Suspense
組件的作用是,當發現其包裹的組件拋出異常且異常為 Promise
對象時,會渲染 fallback
中的內容,即 <p>Loading user...</p>
。等到 Promise
對象 resolve
的時候會再次觸發重新渲染,顯示其包裹的內容,又因為獲取文章列表的時間比用戶短,所以這里會同時顯示用戶信息及其文章列表(具體過程后續會再進行分析)。這樣,通過 Suspense
組件,我們就解決了前面的兩個問題。
同時,使用 Suspense
還會有另外一個好處,假設我們現在改變我們的需求,允許用戶信息和文章列表獨立渲染,則使用 Suspense
重構起來會比較簡單:
而如果使用原來的方式,則需要修改的地方比較多:
可見,使用 Suspense
會帶來很多好處。當然,上文為了方便說明,寫得非常簡單,實際開發時會結合 Relay 這樣的庫來使用,由于這一款目前還處于試驗階段,所以暫時先不做過多的討論。
Suspense
除了可以用于上面的數據獲取這種場景外,還可以用來實現 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> ) }
這也是我們實現遠程組件的基本原理。
介紹了這么多關于 Suspense
的內容后,你一定很好奇它到底是如何實現的吧,我們先不研究 React 源碼,先嘗試自己實現一個 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
方法的回調中改變 state
觸發重新渲染。
接下來,我們還是用上面的例子來分析一下整個過程:
export default function Profile() { return ( <Suspense fallback={<p>Loading user...</p>}> <User /> <Suspense fallback={<p>Loading articles...</p>}> <Articles /> </Suspense> </Suspense> ) }
我們知道 React 在渲染時會構建 Fiber Tree,當處理到 User
組件時,React 代碼中會捕獲到異常:
do { try { workLoopConcurrent() break } catch (thrownValue) { handleError(root, thrownValue) } } while (true)
其中,異常處理函數 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 源碼解讀之首次渲染流程中已經介紹過了,此處就不再贅述了。
render
階段后,會形成如下所示的 Fiber 結構:
之后會進入 commit
階段,將 Fiber 對應的 DOM 插入到容器之中:
注意到 Loading articles...
雖然也被插入了,但確是不可見的。
前面提到過 Suspense
的 updateQueue
中保存了 Promise
請求對象,我們需要在其 resolve
以后觸發應用的重新渲染,這一步驟仍然是在 commit
階段實現的:
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 了以后觸發 react 的重新渲染 wakeable.then(retry, retry) } }) } }
到此,相信大家對“React之Suspense提出的背景及使用方法是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。