Suspense 是用来做什么的? suspense 的字面意思就是悬而不决 ,用在平时开发中,就可以理解为还没有完成的事,你不知道啥时候完成。也就是异步,异步加载组件,异步请求数据。
1. 代码拆分 服务于打包优化的代码拆分。lazy 和suspense 配合使用
1 2 3 4 5 6 7 const A = React .lazy (() => import ('./A' ))return ( <Suspense fallback ={ <p > loading</p > }> <Route component ={A} path ="/a" > </Suspense > )
这样在打包代码时,可以显著减少主包的体积,加快加载速度,从而提升用户体验;而当路由切换时,加载新的组件代码,代码加载是异步的过程,此时 suspense 就会进如 fallback,那我们看到的就是 loading,显式的告诉用户正在加载,当代码加载完成就会展示 A 组件的内容,整个 loading 状态不用开发者去控制。
2. 异步加载数据 既然代码拆分可以不用维护 loading,那么加载数据是不是也可以不用维护 loading 状态呢? 当然可以! 但是直接用 axios 或者 fetch 是无法进入 suspense 的 fallback 的,但是 react 提供了一个库供我们使用 react-cache(暂不建议使用的),它具体是做什么的,原理是什么,我们后面在讨论,这里先体验一下效果如何。
现在用的方法,管理 loading 状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function A ( ) { const [loading, setLoading] = useState (false ); const [list, setList] = useState ([]); function getList ( ) { setloading (true ); promise .then ((res ) => { setList (res); }) .finally (() => { setLoading (false ); }); } useEffect (() => { getList (); }, []); return ( <> data... {loading && <p > loading</p > } </> ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 const resource = unstable_createResource ((id ) => { return Promise .resolve ([]) }) function B ( ) { const data = resource.read (0 ) return ( <Suspense fallback ={ <p > loading</p}> data... </Suspense > ) }
可以看到,使用 suspense 的方式,在开发的时候完全不用维护 loading 状态,而且还有一个比较大的差别,B 中的 list 是没有使用 state 的,它获取的点是在 _B 渲染时_,而 A 获取数据则是发生在 A 渲染后
细谈 Suspense 1. 使用方式(触发方式) react-cache
为什么直接使用 axios 或者 fetch 不能进入 suspense 的 fallback 呢,而使用 react-cache 包一下请求,就可以进入 fallback 了呢? 来康康 react-cache 的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 read : function (input ) { readContext (CacheContext );var key = hashInput (input);var result = accessResult (resource, fetch, input, key);switch (result.status ) {case Pending :{ var suspender = result.value ;throw suspender;} case Resolved :{ var \_value = result.value ;return \_value;} case Rejected :{ var error = result.value ;throw error;} default :return undefined ;} },
这里我截取了部分源码,可以看到这是一个类似 promise 的结构,有三种状态,而需要注意的是 pending 状态。 首先看看这个 result 是这个什么东西,可以理解为 result.value 就是传入的数据请求方法,执行返回的 promise 或者说 likePromise,当这个 promise 未决时,会 throw 出这个 promise,而此时 suspense 看到这个 promise 自然就知道还处于数据请求中,就会展示 fallback 中的内容,当这个 promise 已决时,则代表数据请求结束,suspense 就应该展示数据内容。 大致的 suspense 工作的流程就是: markdown 代码解读复制代码 1. 事先 throw 2. 在 completeWork 之前 catch 住 3. 然后添加到 updateQueue 里 4. updateQueue 批量更新
这也就解释了为什么直接使用 axios、fetch 无法展示 suspense 的 fallback。 在理解原理之后,你可以很轻易的写出类似于这样的代码,替代一下 react-cache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function promiseWrapper (promise ) { let status = "pending" ; let result; let likePromise = promise.then ( (r ) => { status = "success" ; result = r; }, (e ) => { status = "error" ; result = e; } ); return { read ( ) { if (status === "pending" ) { throw likePromise; } else if (status === "error" ) { throw result; } else if (status === "success" ) { return result; } }, }; }
2. 组件渲染顺序 挂载后渲染 获取数据后渲染 并行获取数据和渲染组件
上面说到组件的渲染顺序,在没使用 suspense 时,数据获取是在组件渲染完成之后进行的。而使用 suspense 的方式,组件是如何渲染的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const resource = http ();function Foo ( ) { const result = resource.foo .read (); return <div > Foo: {result.name}</div > ; } function Bar ( ) { const result = resource.bar .read (); return <div > Bar: {result.name}</div > ; } function Page ( ) { return ( <React.Suspense fallback ={ <h1 > Loading Foo...</h1 > }> <Foo /> <React.Suspense fallback ={ <h1 > Loading Bar...</h1 > }> <Bar /> </React.Suspense > </React.Suspense > ); }
3. race condition React 组件有它们自己的“生命周期”。组件可能在任意时间点接收到 props 或者更新 state。然而,每一个异步请求同样也有自己的“生命周期”。异步请求的生命周期开始于我们发出请求,结束于我们收到响应报文。
4. 错误边界 由于以上请求的调用方式,们不等待 promise 就直接开始渲染,使得我们不太好使用 promise.catch 去捕获异步中的错误,所以得另辟蹊径 getDerivedStateFromError 生命周期函数允许我们捕获组件中出现的错误情况,所以我们可以很方便的写出一个类似这样的 ErrorBoundary 处理组件,去处理下面层级中出现的错误情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 代码解读复制代码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 ; } }`` ` # 总结 说了那么多,Suspense 到底有什么用呢?对于这个问题,可以从不同的角度来回答: 1. 它能让数据获取库与 React 紧密整合。如果一个数据请求库实现了对 Suspense 的支持,那么,在 React 中使用 Suspense 将会是自然不过的事。 2. 它能让你有针对性地安排加载状态的展示。虽然它不干涉数据怎样获取,但它可以让你对应用的视图加载顺序有更大的控制权。 3. 它能够消除 race conditions。即便是用上 await,异步代码还是很容易出错。相比之下,Suspense 更给人同步读取数据的感觉 —— 假定数据已经加载完毕。 总之,我更认为Suspense是一种改变你开发模式的,更好的UI解耦的一种模式。