We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suspense 是React今年推出的一个新特性,在 16.6 已经可以部分使用,不过完整的特性支持要等到2019年中。简单的说,Suspense 可以以更简单的方式来实现异步数据获取。
16.6
Suspense
让我们通过一个示例来理解,假设我们有一个组件,可以加载并显示一个电影的名字和简介。为了演示,我们不会获取一个列表,而是每一个电影单独获取自己的数据。这样就会有两个组件:
List
Movie
假设我们不借助任何 Redux 之类的数据层框架,我们一般会这样实现:
Redux
movie.js
import React, { Component } from 'react'; import Spinner from '../Spinner.js'; import { fetchMovie } from '../api.js'; class Movie extends Component { constructor(props) { super(props) this.state = { loading: true, data: undefined } } componentDidMount() { fetchMovie(this.props.id).then((data) => { this.setState({ loading: false, data: data }) }) } render() { if (this.state.loading) return <Spinner />; return ( <div className="movie"> <h4>{this.state.data.title}</h4> <p>{this.state.data.info}</p> </div> ) } } export default Movie
list.js
import React, { Component } from 'react'; import Movie from './movie.js'; class List extends Component { render() { return ( <div> <h2>热门电影</h2> <Movie id={0} /> <Movie id={1} /> <Movie id={2} /> <Movie id={3} /> </div> ); } } export default List;
这是一段很常见的代码,基本逻辑如下:
那么这么做存在哪些问题呢?
loading
Spinner
首先我们看用 Suspense 是如何写上述组件的:
import React from 'react'; import { fetchMovie } from '../api.js'; import { unstable_createResource as createResource } from "react-cache"; /** * 使用Suspense实现 */ const MovieFetcher = createResource(fetchMovie); export default (props) => { const data = MovieFetcher.read(props.id) return ( <div className="movie"> <h4>{data.title}</h4> <p>{data.info}</p> </div> ) }
有人可能立马发现了,不是说好了用 Suspense 吗,在哪呢?
其实是因为 Suspense 会被放在父组件中,别急,我们先看 Movie 组件。
这里有一个非常反常的地方,就是在 render 中竟然加载了数据,而且就直接用了。显然正常情况下这样是错的。
render
然而事实是这确实是正确的写法,当执行到 return 的时候我们的数据已经获取到了。这正是 Suspense 的神奇之处,他可以在获取异步数据的时候 暂停 渲染,当数据获取到的时候继续渲染。为了搞清楚他是如何做到的,让我们继续看下父组件是怎么样的。
return
暂停
比如我们有一个 list 组件会显示多个 Movie,那么 List 组件如下
list
import React, { Suspense, Component } from 'react'; import Movie from './movie.js'; import Spinner from '../Spinner.js'; class List extends Component { render() { return ( <div> <h2>热门电影</h2> <Suspense maxDuration={1500} fallback={<Spinner />}> <Movie id={0} /> <Movie id={1} /> <Movie id={2} /> <Movie id={3} /> </Suspense> </div> ); } } export default List;
可以看到 Movie 会被一个 Suspense 包裹起来。当Movie在加载数据的时候,Suspense 会降级显示 fallback 中的内容,加载完成后会显示真实的数据。
fallback
原理是这样的:
createResource
Promise
resolve
const data = MovieFetcher.read(props.id)
render children
movie
所以 Suspense 能 暂停 渲染的魔法就是通过异常来实现的。不过这里需要注意, render 被暂停之后,恢复执行的时候是重新执行了 render ,而不是从被暂停的地方继续执行,JS应该无法实现这种自由的暂停和恢复执行。
为了搞清楚Suspense的原理,让我们自己动手实现一个简陋的版本。要实现Suspense的功能,我们只需要实现两个关键点即可:
因为完整的实现会稍微麻烦一些,这里为了演示原理,只做一个简单的实现。我们先实现createResource,只支持一个并发请求(因为我们用了一个全局 result 变量做缓存)。实现原理是这样的:
result
用一个result缓存请求结果,当调用 read 函数的时候先去判断有没有缓存数据,有缓存就返回,没有缓存就抛出一个 promise 异常,并且在resolve的时候缓存数据。
read
promise
// 简单mock,只支持一个请求 // 重要提示:dev模式下会把错误显示出来,所以请在build模式下运行 let result = undefined; export const createResource = (p) => { return { read (a) { if (result) return result; const _p = p(a).then((r) => { result = r; }); throw { p: _p }; // 不要直接抛出 _p,否则react会当做内置的Suspense逻辑处理 } } }
细心的读者可能会发现,为什么不直接抛出 _p 而是用一个对象包装一下呢?这是因为 React 内部有对 Suspense 特性的支持,如果他发现有一个组件抛出了一个 Promise,就必须要求外面有一个 Suspense组件进行处理,而我们自己写的 Suspense 显然 react是不会认可的,因此会显示错误。所以这里用一个对象进行了一次包装。
_p
React
那么 Suspense 应该怎么写呢? Suspense 显然是一个高阶组件,他会包装我们的组件,在未加载的时候显示 fallback ,加载完成的时候显示我们的组件。通过 componentDidCatch 可以捕获 createResource 抛出的异常,并且能在 resolve 的时候读取数据。
componentDidCatch
import React, { Component } from 'react'; export default class extends Component { state = { loading: false }; componentDidCatch(error, info) { this.setState({ loading: true }); error.p.then(() => { this.setState({ loading: false }); }); } render() { return this.state.loading ? this.props.fallback : this.props.children } }
这样我们就自己实现了一个 Suspense,虽然很简陋,不过可以揭示Suspense的原理。
Suspense还有一些其他特性,这里不一一展开。
Suspense是一个争议很大的特性,因为他其实颠覆了React的一些哲学:
实际使用的时候还发现,Suspense 会等待所有的孩子都加载完成之后才能显示,这样如果有一个加载很慢就会导致页面空白很久。
虽然有这些问题,但是这个特性依然有一些好处:
有人可能会问是不是以后就不需要 Redux 之类的数据层框架了?我的看法的是,Suspense 能替代Redux 中获取数据,缓存数据的能力,但是无法替代跨层级的数据通信能力。因此对于需要经常进行跨层级组件通信的复杂应用来说,Suspense 依然无法满足需要。
这篇博客里面提到的全部代码都在这个仓库中可以找到,包括三种不同实现方式的代码:https://github.com/lihongxun945/suspense-demo
参考 https://medium.com/@Charles_Stover/react-suspense-with-the-fetch-api-a1b7369b0469
The text was updated successfully, but these errors were encountered:
No branches or pull requests
Suspense 是什么
Suspense 是React今年推出的一个新特性,在
16.6
已经可以部分使用,不过完整的特性支持要等到2019年中。简单的说,Suspense
可以以更简单的方式来实现异步数据获取。让我们通过一个示例来理解,假设我们有一个组件,可以加载并显示一个电影的名字和简介。为了演示,我们不会获取一个列表,而是每一个电影单独获取自己的数据。这样就会有两个组件:
List
组件会是电影列表,其中每一个电影信息都是一个Movie
组件Movie
组件会负责加载并显示指定id的电影信息假设我们不借助任何
Redux
之类的数据层框架,我们一般会这样实现:movie.js
list.js
这是一段很常见的代码,基本逻辑如下:
那么这么做存在哪些问题呢?
loading
标记Movie
的时候会出现多个Spinner
那么
Suspense
是如何解决这些问题的?suspense 如何解决
首先我们看用
Suspense
是如何写上述组件的:有人可能立马发现了,不是说好了用
Suspense
吗,在哪呢?其实是因为
Suspense
会被放在父组件中,别急,我们先看Movie
组件。这里有一个非常反常的地方,就是在
render
中竟然加载了数据,而且就直接用了。显然正常情况下这样是错的。然而事实是这确实是正确的写法,当执行到
return
的时候我们的数据已经获取到了。这正是Suspense
的神奇之处,他可以在获取异步数据的时候暂停
渲染,当数据获取到的时候继续渲染。为了搞清楚他是如何做到的,让我们继续看下父组件是怎么样的。比如我们有一个
list
组件会显示多个Movie
,那么List
组件如下可以看到
Movie
会被一个Suspense
包裹起来。当Movie在加载数据的时候,Suspense 会降级显示fallback
中的内容,加载完成后会显示真实的数据。原理是这样的:
createResource
加载数据的时候会进行缓存,如果有缓存就直接用,当没有缓存的时候会抛出一个异常并异步加载数据,这个异常是一个Promise
,加载完成后会缓存数据并resolve
Movie
加载数据的时候会抛出异常,因此执行到const data = MovieFetcher.read(props.id)
会由于抛出异常而停止执行Suspense
会捕获这个异常,然后显示 fallback 中的内容resolve
之后,Suspense
才会render children
。也就是movie
的render
函数会被重新调用,因为已经有数据了,就可以正常渲染了。所以
Suspense
能暂停
渲染的魔法就是通过异常来实现的。不过这里需要注意,render
被暂停之后,恢复执行的时候是重新执行了render
,而不是从被暂停的地方继续执行,JS应该无法实现这种自由的暂停和恢复执行。模拟实现方式
为了搞清楚Suspense的原理,让我们自己动手实现一个简陋的版本。要实现Suspense的功能,我们只需要实现两个关键点即可:
createResource
函数,他会在数据没有加载的时候返回一个Promise
,并且在加载到数据之后resolve
Suspense
高阶组件,如果发现子组件抛出了一个Promise
错误,则显示fallback
,当resolve
之后就显示孩子。因为完整的实现会稍微麻烦一些,这里为了演示原理,只做一个简单的实现。我们先实现
createResource
,只支持一个并发请求(因为我们用了一个全局result
变量做缓存)。实现原理是这样的:用一个result缓存请求结果,当调用
read
函数的时候先去判断有没有缓存数据,有缓存就返回,没有缓存就抛出一个promise
异常,并且在resolve的时候缓存数据。细心的读者可能会发现,为什么不直接抛出
_p
而是用一个对象包装一下呢?这是因为React
内部有对Suspense
特性的支持,如果他发现有一个组件抛出了一个Promise
,就必须要求外面有一个Suspense
组件进行处理,而我们自己写的Suspense
显然 react是不会认可的,因此会显示错误。所以这里用一个对象进行了一次包装。那么
Suspense
应该怎么写呢?Suspense
显然是一个高阶组件,他会包装我们的组件,在未加载的时候显示fallback
,加载完成的时候显示我们的组件。通过componentDidCatch
可以捕获createResource
抛出的异常,并且能在resolve
的时候读取数据。这样我们就自己实现了一个
Suspense
,虽然很简陋,不过可以揭示Suspense
的原理。Suspense 的其他特性
Suspense还有一些其他特性,这里不一一展开。
suspense的优缺点
Suspense是一个争议很大的特性,因为他其实颠覆了React的一些哲学:
实际使用的时候还发现,
Suspense
会等待所有的孩子都加载完成之后才能显示,这样如果有一个加载很慢就会导致页面空白很久。虽然有这些问题,但是这个特性依然有一些好处:
有人可能会问是不是以后就不需要
Redux
之类的数据层框架了?我的看法的是,Suspense
能替代Redux
中获取数据,缓存数据的能力,但是无法替代跨层级的数据通信能力。因此对于需要经常进行跨层级组件通信的复杂应用来说,Suspense
依然无法满足需要。资源
这篇博客里面提到的全部代码都在这个仓库中可以找到,包括三种不同实现方式的代码:https://github.com/lihongxun945/suspense-demo
参考
https://medium.com/@Charles_Stover/react-suspense-with-the-fetch-api-a1b7369b0469
The text was updated successfully, but these errors were encountered: