本文介紹與 Suspense
在三種情景下使用方法,並結合源碼進行相應解析。歡迎關注我的博客。html
在 16.6 版本以前,code-spliting
一般是由第三方庫來完成的,好比 react-loadble(核心思路爲: 高階組件 + webpack dynamic import), 在 16.6 版本中提供了 Suspense
和 lazy
這兩個鉤子, 所以在以後的版本中即可以使用其來實現 Code Spliting
。react
目前階段, 服務端渲染中的
code-spliting
仍是得使用react-loadable
, 可查閱 React.lazy, 暫時先不探討緣由。webpack
Code Spliting
在 React
中的使用方法是在 Suspense
組件中使用 <LazyComponent>
組件:git
import { Suspense, lazy } from 'react' const DemoA = lazy(() => import('./demo/a')) const DemoB = lazy(() => import('./demo/b')) <Suspense> <NavLink to="/demoA">DemoA</NavLink> <NavLink to="/demoB">DemoB</NavLink> <Router> <DemoA path="/demoA" /> <DemoB path="/demoB" /> </Router> </Suspense>
源碼中 lazy
將傳入的參數封裝成一個 LazyComponent
github
function lazy(ctor) { return { $$typeof: REACT_LAZY_TYPE, // 相關類型 _ctor: ctor, _status: -1, // dynamic import 的狀態 _result: null, // 存放加載文件的資源 }; }
觀察 readLazyComponentType 後能夠發現 dynamic import
自己相似 Promise
的執行機制, 也具備 Pending
、Resolved
、Rejected
三種狀態, 這就比較好理解爲何 LazyComponent
組件須要放在 Suspense
中執行了(Suspense
中提供了相關的捕獲機制, 下文會進行模擬實現`), 相關源碼以下:web
function readLazyComponentType(lazyComponent) { const status = lazyComponent._status; const result = lazyComponent._result; switch (status) { case Resolved: { // Resolve 時,呈現相應資源 const Component = result; return Component; } case Rejected: { // Rejected 時,throw 相應 error const error = result; throw error; } case Pending: { // Pending 時, throw 相應 thenable const thenable = result; throw thenable; } default: { // 第一次執行走這裏 lazyComponent._status = Pending; const ctor = lazyComponent._ctor; const thenable = ctor(); // 能夠看到和 Promise 相似的機制 thenable.then( moduleObject => { if (lazyComponent._status === Pending) { const defaultExport = moduleObject.default; lazyComponent._status = Resolved; lazyComponent._result = defaultExport; } }, error => { if (lazyComponent._status === Pending) { lazyComponent._status = Rejected; lazyComponent._result = error; } }, ); // Handle synchronous thenables. switch (lazyComponent._status) { case Resolved: return lazyComponent._result; case Rejected: throw lazyComponent._result; } lazyComponent._result = thenable; throw thenable; } } }
爲了解決獲取的數據在不一樣時刻進行展示的問題(在 suspenseDemo 中有相應演示), Suspense
給出瞭解決方案。算法
下面放兩段代碼,能夠從中直觀地感覺在 Suspense
中使用 Async Data Fetching
帶來的便利。redux
export default class Demo extends Component { state = { data: null, }; componentDidMount() { fetchAPI(`/api/demo/${this.props.id}`).then((data) => { this.setState({ data }); }); } render() { const { data } = this.state; if (data == null) { return <Spinner />; } const { name } = data; return ( <div>{name}</div> ); } }
Suspense
中進行數據獲取的代碼以下:const resource = unstable_createResource((id) => { return fetchAPI(`/api/demo`) }) function Demo { render() { const data = resource.read(this.props.id) const { name } = data; return ( <div>{name}</div> ); } }
能夠看到在 Suspense
中進行數據獲取的代碼量相比正常的進行數據獲取的代碼少了將近一半!少了哪些地方呢?api
loading
狀態的維護(在最外層的 Suspense 中統一維護子組件的 loading)當前 Suspense
的使用分爲三個部分:promise
第一步: 用 Suspens
組件包裹子組件
import { Suspense } from 'react' <Suspense fallback={<Loading />}> <ChildComponent> </Suspense>
第二步: 在子組件中使用 unstable_createResource
:
import { unstable_createResource } from 'react-cache' const resource = unstable_createResource((id) => { return fetch(`/demo/${id}`) })
第三步: 在 Component
中使用第一步建立的 resource
:
const data = resource.read('demo')
來看下源碼中 unstable_createResource
的部分會比較清晰:
export function unstable_createResource(fetch, maybeHashInput) { const resource = { read(input) { ... const result = accessResult(resource, fetch, input, key); switch (result.status) { case Pending: { const suspender = result.value; throw suspender; } case Resolved: { const value = result.value; return value; } case Rejected: { const error = result.value; throw error; } default: // Should be unreachable return (undefined: any); } }, }; return resource; }
結合該部分源碼, 進行以下推測:
throw
一個 thenable
對象, Suspense
組件內的 componentDidCatch
捕獲之, 此時展現 Loading
組件;Promise
態的對象變爲完成態後, 頁面刷新此時 resource.read()
獲取到相應完成態的值;LRU
緩存算法, 跳過 Loading
組件返回結果(緩存算法見後記);官方做者是說法以下:
因此說法大體相同, 下面實現一個簡單版的 Suspense
:
class Suspense extends React.Component { state = { promise: null } componentDidCatch(e) { if (e instanceof Promise) { this.setState({ promise: e }, () => { e.then(() => { this.setState({ promise: null }) }) }) } } render() { const { fallback, children } = this.props const { promise } = this.state return <> { promise ? fallback : children } </> } }
進行以下調用
<Suspense fallback={<div>loading...</div>}> <PromiseThrower /> </Suspense> let cache = ""; let returnData = cache; const fetch = () => new Promise(resolve => { setTimeout(() => { resolve("數據加載完畢"); }, 2000); }); class PromiseThrower extends React.Component { getData = () => { const getData = fetch(); getData.then(data => { returnData = data; }); if (returnData === cache) { throw getData; } return returnData; }; render() { return <>{this.getData()}</>; } }
效果調試能夠點擊這裏, 在 16.6
版本以後, componentDidCatch
只能捕獲 commit phase
的異常。因此在 16.6
版本以後實現的 <PromiseThrower>
又有一些差別(即將 throw thenable
移到 componentDidMount
中進行)。
當網速足夠快, 數據立馬就獲取到了,此時頁面存在的 Loading
按鈕就顯得有些多餘了。(在 suspenseDemo 中有相應演示), Suspense
在 Concurrent Mode
下給出了相應的解決方案, 其提供了 maxDuration
參數。用法以下:
<Suspense maxDuration={500} fallback={<Loading />}> ... </Suspense>
該 Demo 的效果爲當獲取數據的時間大於(是否包含等於還沒確認) 500 毫秒, 顯示自定義的 <Loading />
組件, 當獲取數據的時間小於 500 毫秒, 略過 <Loading>
組件直接展現用戶的數據。相關源碼。
須要注意的是 maxDuration
屬性只有在 Concurrent Mode
下才生效, 可參考源碼中的註釋。在 Sync 模式下, maxDuration
始終爲 0。
LRU
算法: Least Recently Used
最近最少使用算法(根據時間);LFU
算法: Least Frequently Used
最近最少使用算法(根據次數);若數據的長度限定是 3, 訪問順序爲 set(2,2),set(1,1),get(2),get(1),get(2),set(3,3),set(4,4)
, 則根據 LRU
算法刪除的是 (3, 3)
, 根據 LFU
算法刪除的是 (1, 1)
。
react-cache
採用的是 LRU
算法。
Suspense
開發進度