## 1 引言 很多人都用过 React Suspense,但如果你认为它只是配合 React.lazy 实现异步加载的蒙层,就理解的太浅了。实际上,React Suspense 改变了开发规则,要理解这一点,需要作出思想上的改变。 我们结合 [Why React Suspense Will Be a Game Changer](https://medium.com/react-in-depth/why-react-suspense-will-be-a-game-changer-37b40fea71ec) 这篇文章,带你重新认识 React Suspense。 ## 2 概述 异步加载是前端开发的重要环节,也是一直以来样板代码最严重的场景之一,原文通过三种取数方案的对比,逐渐找到一种最佳的异步取数方式。 在讲解这三种取数方案之前,首先通过下面这张图说明了 Suspense 的功能: ![](https://img.alicdn.com/tfs/TB12.npyoz1gK0jSZLeXXb9kVXa-1024-808.gif) 从上图可以看出,子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 `fallback`,当所有子组件异步阻塞取消后才会正常渲染。 下面介绍文中给出的三种取数方式,首先是最原始的本地状态管理方案。 ### 本地异步状态管理,直白但不利于维护 在 Suspense 方案出来之前,我们一般都在代码中利用本地状态管理异步数据。 即便代码做了一定抽象,那也只是把逻辑从一个文件移到了另一个问题,可维护性与可拓展性都没有本质的改变,因此基本可以用下面的结构说明: ```javascript 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 ? (
Loading...
) : error ? (Error: {error}
) : (Data loaded ?
); } } ``` 如上所述,首先申明本地状态管理至少三种数据:异步状态、异步结果与异步错误,其次在不同的生命周期中处理初始化发请求与重新发请求的问题,最后在渲染函数中根据不同的状态渲染不同的结果,所以实际上我们写了三个渲染组件。 从下面几个角度对上述代码进行评价: - **冗余的三种状态 - 糟糕的开发体验** - 很明显,存储了三套数据,渲染三种结果,不利于开发维护。 - **冗余的样板代码 - 糟糕的开发体验** - 为了管理异步状态,上述代码非常冗长,显然这个问题是存在的。 - **数据与状态封闭性 - 糟糕的用户体验 + 开发体验** - 所有数据与状态管理都存储在每一个这种组件中,将取数状态与组件绑定的结果就是,我们只能忍受组件独立运行的 Loading 逻辑,而无法对他们进行统一管理。 - **重新取数 - 糟糕的开发体验** - 需要在另一个生命周期中申明重新取数,很明显是个麻烦的行为。 - **一闪而过的短暂 Loading - 糟糕的用户体验** - 如果用户网速足够快,则 Loading 时间会非常短,此时一闪而过的 Loading 反而比没有 Loading 更烦人,我们应该在用户感知到卡的时候再出现 Loading 状态。 ### Context 管理状态,有进步但问题依然很多 如果利用 Context 做状态共享,我们将取数的数据管理与逻辑代码写在父组件,子组件专心用于展示,效果会好一些,代码如下: ```javascript 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() { returnLoading...
) : idData.error ? (Error: {idData.error}
) : (Data loaded ?
); } } ``` `DataContextProvider` 组件承担了状态管理与异步逻辑工作,而 `DynamicData` 组件只需要从 Context 获取异步状态渲染即可,这样来看至少解决了一部分问题,我们还是从之前的角度进行评价: - **冗余的三种状态 - 糟糕的开发体验** - 问题依然存在,只不过代码的位置转移了一部分到父组件。 - **冗余的样板代码 - 糟糕的开发体验** - 将展示与逻辑分离,成功降低了样板代码数量,至少当一个异步数据复用于多个组件时,不需要写多份样板代码了。 - **数据与状态封闭性 - 糟糕的用户体验 + 开发体验** - 这个问题得到一定程度解决,但是引入了新问题,即这个子组件仅在特定环境下可以正常运行。但在一个良好的设计下,组件运行不应该依赖于它所处的位置。 - **重新取数 - 糟糕的开发体验** - 问题依然存在。 - **一闪而过的短暂 Loading - 糟糕的用户体验** - 问题依然存在。 ### Suspense 管理状态,最棒的方案 利用 Suspense 进行异步处理,代码处理大概是这样的: ```javascript import createResource from "./magical-cache-provider"; const dataResource = createResource(id => fetchData(id)); class DynamicData extends Component { render() { const data = dataResource.read(this.props.id); returnData loaded ?
; } } class App extends Component { render() { return (