本文介绍与 Suspense
在三种情景下使用方法,并结合源码进行相应解析。欢迎关注个人博客。
Code Spliting
在 16.6 版本之前,code-spliting
通常是由第三方库来完成的,比如 react-loadble(核心思路为: 高阶组件 + webpack dynamic import), 在 16.6 版本中提供了 Suspense
和 lazy
这两个钩子, 因此在之后的版本中便可以使用其来实现 Code Spliting
。
目前阶段, 服务端渲染中的
code-spliting
还是得使用react-loadable
, 可查阅 React.lazy, 暂时先不探讨原因。
Code Spliting
在 React
中的使用方法是在 Suspense
组件中使用
组件:
import { Suspense, lazy } from 'react'
const DemoA = lazy(() => import('./demo/a'))
const DemoB = lazy(() => import('./demo/b'))
DemoA
DemoB
源码中 lazy
将传入的参数封装成一个 LazyComponent
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
中提供了相关的捕获机制, 下文会进行模拟实现`), 相关源码如下:
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;
}
}
}
Async Data Fetching
为了解决获取的数据在不同时刻进行展现的问题(在 suspenseDemo 中有相应演示), Suspense
给出了解决方案。
下面放两段代码,可以从中直观地感受在 Suspense
中使用 Async Data Fetching
带来的便利。
- 一般进行数据获取的代码如下:
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 ;
}
const { name } = data;
return (
{name}
);
}
}
- 在
Suspense
中进行数据获取的代码如下:
const resource = unstable_createResource((id) => {
return fetchAPI(`/api/demo`)
})
function Demo {
render() {
const data = resource.read(this.props.id)
const { name } = data;
return (
{name}
);
}
}
可以看到在 Suspense
中进行数据获取的代码量相比正常的进行数据获取的代码少了将近一半!少了哪些地方呢?
- 减少了
loading
状态的维护(在最外层的 Suspense 中统一维护子组件的 loading) - 减少了不必要的生命周期的书写
总结: 如何在 Suspense 中使用 Data Fetching
当前 Suspense
的使用分为三个部分:
第一步: 用 Suspens
组件包裹子组件
import { Suspense } from 'react'
}>
第二步: 在子组件中使用 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 }
>
}
}
进行如下调用
loading...