目前Suspense尚处于实验阶段,大部分文档尚未被翻译。我基于目前的官方文档,对Suspense做一些介绍。html
⚠️⚠️⚠️本文是概念性的,主要介绍
Suspense
解决了那些问题,而不是正确的使用方法。目前Facebook只在生产中,使用了Suspense
与Relay
的集成方案。若是你不使用Relay
,可能须要等待一段时间才能在应用程序中真正的使用Suspense
。(本文示例中的代码是"伪"代码,真实的实现可能要复杂的多,示例代码不要复制粘贴到你的项目中。)react
Suspense
是React16.6版本中新增的组件,容许咱们等待一些代码的加载,并在等待时声明加载状态。api
// 使用React.lazy以及Suspense进行代码分割的例子
const Foo = React.lazy(() => import('./Foo'))
function Component () {
return (
<Suspense fallback={<Spinner />}> <Component /> </Suspense>
)
}
复制代码
Suspense for Data是一个新的特性。容许您使用Suspense
等待任何其余内容。包括Ajax请求异步返回的数据。(本文着重于介绍异步获取数据的例子)。Suspense
可让组件在渲染以前进行等待。Suspense
是一种通讯机制,告知组件数据还没有准备就绪,React会等待它准备好后更新UI。promise
const apiParent = () => {
return new Promise(resolve => {
setTimeout(() => resolve({ name: 'parent' }), 1000)
})
}
const apiChild = () => {
return new Promise(resolve => {
setTimeout(() => resolve({ name: 'child' }), 2000)
})
}
// pending 时,wrapPromise会抛出一个Promise
// resolve 时,wrapPromise会返回结果
const wrapPromise = (promise) => {
let status = 'pending'
let result = null
let suspender = promise.then(res => {
status = 'success'
result = res
}).catch(err => {
status = 'error'
result = err
})
return {
read() {
if (status === 'pending') {
throw suspender
} else if (status === 'error') {
throw result
} else if (status === 'success') {
return result
}
}
}
}
const http = () => {
const parentPromise = apiParent()
const childPromise = apiChild()
return {
parent: wrapPromise(parentPromise),
child: wrapPromise(childPromise)
}
}
const resource = http()
function Parent () {
const result = resource.parent.read()
return <div>Parent: { result.name }</div>
}
function Child () {
const result = resource.child.read()
return <div>Child: { result.name }</div>
}
复制代码
function App() {
return (
<div className="App">
{/* 在Parent没有返回结果前,显示<h1>Loading Parent...</h1> */}
<React.Suspense fallback={<h1>Loading Parent...</h1>}>
<Parent/>
{/* 在Child没有返回结果前,显示<h1>Loading Child...</h1> */}
<React.Suspense fallback={<h1>Loading Child...</h1>}>
<Child/>
</React.Suspense>
</React.Suspense>
</div>
)
}
复制代码
在实际开发一个应用时,应该根据需求混合使用不一样的方法。这里区别看待,只是为了更好的权衡它们的取舍。dom
咱们彻底能够在不说起其余数据获取方法的状况下,介绍 Suspense
。可是这样咱们就难以知道,Suspense
解决了那些问题,以及Suspense
与如今的方案有那些不一样。异步
渲染时请求数据,是React应用中经常使用的获取数据的方法。由于它直到组件在屏幕上进行渲染后,才开始请求数据。会致使所谓的“瀑布”问题。ui
function Foo () {
const [state, setState] = useState('')
useEffect(() => {
api().then(res => setState(res))
}, [])
if (!state) return <div>Loading Foo……</div>
return (
<div>Foo</div>
)
}
function Bar () {
const [state, setState] = useState('')
useEffect(() => {
api().then(res => setState(res))
}, [])
if (!state) return <div>Loading Bar……</div>
return (
<React.Fragment> <div>Bar</div> <Foo/> </React.Fragment> ) } function App() { return ( <div className="App"> <Bar/> </div> ) } 复制代码
考虑上面的代码。代码的执行顺序将会是this
若是获取Bar组件的数据,须要花费3秒。那么,咱们只能在3秒后,开始获取Foo组件的数据。这就是“瀑布问题”, 应该被并行处理的请求序列。spa
咱们能够使用Promise.all
避免瀑布问题。翻译
const api = (ms = 1000, type) => {
return new Promise(resolve => {
setTimeout(() => resolve('result'), ms)
})
}
const fakeHttp = () => {
return Promise.all([api(1000, 'bar'), api(3000, 'foo')])
}
const promise = fakeHttp()
function Foo (props) {
if (!props.state) return <div>Loading Foo……</div>
return (
<div>Foo</div>
)
}
function Bar () {
const [state, setState] = useState('')
useEffect(() => {
promise.then(res => setState(true))
}, [])
if (!state) return <div>Loading Bar……</div>
return (
<React.Fragment>
<div>Bar</div>
<Foo state={state}/>
</React.Fragment>
)
}
复制代码
考虑上面的代码。代码的执行顺序将会是
咱们解决了瀑布问题。可是却映入了另外一个问题,咱们必须等待全部数据返回后才开始渲染。
虽然咱们能够把请求从Promise.all
拆开,分别发起两个Promise,可是随着组件树愈加的复杂,这显然不是一个好主意,维护起来将会至关的困难。
在以前的方法中。咱们的步骤都是
使用Suspense
后,咱们能够无需等待响应返回就开始渲染。
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>
)
}
function App() {
return (
<div className="App">
<Page/>
</div>
)
}
复制代码
resource.foo.read()
没有返回数据,组件被挂起。React跳过它,尝试渲染树中的其余组件。resource.bar.read()
没有返回数据,组件被挂起,React跳过它。Suspense fallback
。Suspense fallback
将会消失。当咱们调用read()方法时,要么获取数据,要么将组件挂起
使用Suspense
能够帮助咱们消除if (statr) return loading
这样的的模版代码。咱们还能够根据须要,增删Suspense
组件控制加载状态的粒度(好比,两个列表的状况下。我只想要一个加载态,能够在两个列表的外面,统一添加一层Suspense
边界。若是需想要两个加载态,能够给各个列表各添加一个Suspense
边界)而无需对组件代码进行侵入式的修改。
const api = (id) => {
const ms = getRandomTime()
return new Promise(resolve => {
setTimeout(() => resolve(id), ms)
})
}
function Bar (props) {
const { id, clickNumber } = props
if (!id) return <h1>Loading……</h1>
return (
<div>state: { id } clickNumber: { clickNumber }</div>
)
}
let id = 0
let clickNumber = 0
function Page () {
const [selfId, setSelfId] = useState(id)
return (
<>
<button onClick={() => {
id += 1
clickNumber += 1
api(id).then((id) => setSelfId(id))
}}>+</button>
<Bar id={selfId} clickNumber={clickNumber}/>
</>
)
}
复制代码
在上面的代码,接口返回的结果可能存在“竞态”的问题。
由于每一次接口响应返回时间是不肯定的,因此可能存在前一次的返回的结果,覆盖后一次的状况。而使用Suspense
能够很好的解决竞态的问题。下面咱们使用Suspense
重写示例。
const api = (id) => {
const ms = getRandomTime()
return new Promise(resolve => {
setTimeout(() => resolve(id), ms)
})
}
const http = (id) => {
return wrapPromise(api(id))
}
function Bar (props) {
const { resource, clickNumber } = props
const id = resource.read()
return (
<div>state: { id } clickNumber: { clickNumber }</div>
)
}
let id = 0
let clickNumber = 0
const initResource = http(id)
function Page () {
const [resource, setResource] = useState(initResource)
return (
<>
<button onClick={() => {
id += 1
clickNumber += 1
setResource(http(id))
}}>+</button>
<React.Suspense fallback={<h1>Loading……</h1>}>
<Bar resource={resource} clickNumber={clickNumber}/>
</React.Suspense>
</>
)
}
复制代码
在Suspense
版本的例子中,咱们不须要等待响应结束后设置组件的状态,这样很容易出错,由于咱们须要考虑设置对应状态的时机。咱们直接传给子组件资源对象resource,只要resource.read
没有返回数据,组件将一直处于挂起的状态,当props.resource
更新,从新请求,组件依然处于挂起的状态,只到resource.read
返回数据,组件才会被从新渲染,咱们就不须要考虑竞态的问题。
当异步请求发生了错误,Suspense
能够借助“错误边界”捕获异步请求抛出的错误。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
render () {
if (this.state.hasError) {
return <h1>:( error</h1>
}
return this.props.children;
}
}
function Page () {
const [resource, setResource] = useState(initResource)
return (
<>
<button onClick={() => {
id += 1
clickNumber += 1
setResource(http(id))
}}>+</button>
{/* 使用错误边界捕获异步错误 */}
<ErrorBoundary>
<React.Suspense fallback={<h1>Loading……</h1>}>
<Bar resource={resource} clickNumber={clickNumber}/>
</React.Suspense>
</ErrorBoundary>
</>
)
}
复制代码
上面仅是做者本身的理解,若有错误请及时指出。