不少人都用过 React Suspense,但若是你认为它只是配合 React.lazy 实现异步加载的蒙层,就理解的太浅了。实际上,React Suspense 改变了开发规则,要理解这一点,须要做出思想上的改变。javascript
咱们结合 Why React Suspense Will Be a Game Changer 这篇文章,带你从新认识 React Suspense。前端
异步加载是前端开发的重要环节,也是一直以来样板代码最严重的场景之一,原文经过三种取数方案的对比,逐渐找到一种最佳的异步取数方式。java
在讲解这三种取数方案以前,首先经过下面这张图说明了 Suspense 的功能:react
从上图能够看出,子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 fallback
,当全部子组件异步阻塞取消后才会正常渲染。git
下面介绍文中给出的三种取数方式,首先是最原始的本地状态管理方案。github
在 Suspense 方案出来以前,咱们通常都在代码中利用本地状态管理异步数据。promise
即使代码作了必定抽象,那也只是把逻辑从一个文件移到了另外一个问题,可维护性与可拓展性都没有本质的改变,所以基本能够用下面的结构说明:微信
class DynamicData extends Component {
state = {
loading: true,
error: null,
data: null
};
componentDidMount() {
fetchData(this.props.id)
.then(data => {
this.setState({
loading: false,
data
});
})
.catch(error => {
this.setState({
loading: false,
error: error.message
});
});
}
componentDidUpdate(prevProps) {
if (this.props.id !== prevProps.id) {
this.setState({ loading: true }, () => {
fetchData(this.props.id)
.then(data => {
this.setState({
loading: false,
data
});
})
.catch(error => {
this.setState({
loading: false,
error: error.message
});
});
});
}
}
render() {
const { loading, error, data } = this.state;
return loading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error}</p>
) : (
<p>Data loaded ?</p>
);
}
}
复制代码
如上所述,首先申明本地状态管理至少三种数据:异步状态、异步结果与异步错误,其次在不一样的生命周期中处理初始化发请求与从新发请求的问题,最后在渲染函数中根据不一样的状态渲染不一样的结果,因此实际上咱们写了三个渲染组件。异步
从下面几个角度对上述代码进行评价:ide
若是利用 Context 作状态共享,咱们将取数的数据管理与逻辑代码写在父组件,子组件专心用于展现,效果会好一些,代码以下:
const DataContext = React.createContext();
class DataContextProvider extends Component {
// We want to be able to store multiple sources in the provider,
// so we store an object with unique keys for each data set +
// loading state
state = {
data: {},
fetch: this.fetch.bind(this)
};
fetch(key) {
if (this.state[key] && (this.state[key].data || this.state[key].loading)) {
// Data is either already loaded or loading, so no need to fetch!
return;
}
this.setState(
{
[key]: {
loading: true,
error: null,
data: null
}
},
() => {
fetchData(key)
.then(data => {
this.setState({
[key]: {
loading: false,
data
}
});
})
.catch(e => {
this.setState({
[key]: {
loading: false,
error: e.message
}
});
});
}
);
}
render() {
return <DataContext.Provider value={this.state} {...this.props} />; } } class DynamicData extends Component { static contextType = DataContext; componentDidMount() { this.context.fetch(this.props.id); } componentDidUpdate(prevProps) { if (this.props.id !== prevProps.id) { this.context.fetch(this.props.id); } } render() { const { id } = this.props; const { data } = this.context; const idData = data[id]; return idData.loading ? ( <p>Loading...</p> ) : idData.error ? ( <p>Error: {idData.error}</p> ) : ( <p>Data loaded ?</p> ); } } 复制代码
DataContextProvider
组件承担了状态管理与异步逻辑工做,而 DynamicData
组件只须要从 Context 获取异步状态渲染便可,这样来看至少解决了一部分问题,咱们仍是从以前的角度进行评价:
利用 Suspense 进行异步处理,代码处理大概是这样的:
import createResource from "./magical-cache-provider";
const dataResource = createResource(id => fetchData(id));
class DynamicData extends Component {
render() {
const data = dataResource.read(this.props.id);
return <p>Data loaded ?</p>;
}
}
class App extends Component {
render() {
return (
<Suspense fallback={<p>Loading...</p>}> <DeepNesting> <DynamicData /> </DeepNesting> </Suspense>
);
}
}
复制代码
在原文写做的时候,Suspense 仅能对 React.lazy 生效,但如今已经能够对任何异步状态生效了,只要符合 Pending 中 throw promise 的规则。
咱们再审视一下上面的代码,能够发现代码量减小了不少,其中和转换成 Function Component 的写法也有关系。
最后仍是从以下几个角度进行评价:
为了进一步说明 Suspense 的魔力,笔者特地把这段代码单独拿出来讲明:
class App extends Component {
render() {
return (
<Suspense fallback={<p>Loading...</p>}> <DeepNesting> <MaybeSomeAsycComponent /> <Suspense fallback={<p>Loading content...</p>}> <ThereMightBeSeveralAsyncComponentsHere /> </Suspense> <Suspense fallback={<p>Loading footer...</p>}> <DeeplyNestedFooterTree /> </Suspense> </DeepNesting> </Suspense>
);
}
}
复制代码
上面代码代表了逻辑与展现的完美分离。
从代码结构上来看,咱们能够在任何须要异步取数的组件父级添加 Suspense 达到 Loading 的效果,也就是说,若是只在最外层加一个 Suspense,那么整个应用全部 Loading 都结束后才会渲染,然而咱们也能为所欲为的在任何层级继续添加 Suspense,那么对应做用域内的 Loading 就会首先执行完毕,并由当前的 Suspense 控制。
这意味着咱们能够自由决定 Loading 状态的范围组合。 试想当 Loading 状态交由组件控制的方案一与方案二,是不可能作到合并 Loading 时机的,而 Suspense 方案作到了将 Loading 状态与 UI 分离,咱们能够经过添加 Suspense 自由控制 Loading 的粒度。
Suspense 对全部子组件异步均可以做用,所以不管是 React.lazy 仍是异步取数,均可以经过 Suspense 进行 Pending。
异步时机被 Suspense pending 须要遵循必定规则,这个规则在以前的 精读《Hooks 取数 - swr 源码》 有介绍过,即 Suspense 要求代码 suspended,即抛出一个能够被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件,所以取数函数须要在 Pending 状态时抛出一个 Promise,使其能够被 Suspense 捕获到。
另外,关于文中提到的 fallback 最小出现时间的保护间隔,目前仍是一个 Open Issue,也许有一天 React 官方会提供支持。
不过即使官方不支持,咱们也有方式实现,即让这个逻辑由 fallback 组件实现:
<Suspense fallback={MyFallback} />;
const MyFallback = () => {
// 计时器,200 ms 之内 return null,200 ms 后 return <Spin />
};
复制代码
之因此说 Suspense 开发方式改变了开发规则,是由于它作到了将异步的状态管理与 UI 组件分离,全部 UI 组件都无需关心 Pending 状态,而是看成同步去执行,这自己就是一个巨大的改变。
另外因为状态的分离,咱们能够利用纯 UI 组件拼装任意粒度的 Pending 行为,以整个 App 做为一个大的 Suspense 做为兜底,这样 UI 完全与异步解耦,哪里 Loading,什么范围内 Loading,彻底由 Suspense 组合方式决定,这样的代码显然具有了更强的可拓展性。
若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)