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解耦的一种模式。