Skip to content

⚠️ Important Notice

This post was last updated on: which was . Please pay attention to its timelines.

Suspense

介绍

React16.6之后新增了<Suspense />组件,可以作为组件的等待边界,当组件是从服务端加载或者懒加载时,会渲染fallback内容,避免白屏,等待加载完成之后显示组件内容。

一般都是与lazy()配合使用,该方法是用来加载某个组件的,结合一些Boundler工具可以进行代码分割等优化操作。

js
function lazy<T extends ComponentType<any>>(
    factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;

该方法目前(React18)只能和<Suspense />组件使用(当然,要是实现一个和Suspense组件类似的也能一起使用)。

但是Suspense能和其他数据加载的方式一起使用:

jsx
import { Suspense } from 'react'

let data = ''
function fetchData() {
  if(data) return data
	throw new Promise((resolve) => {
		setTimeout(() => {
			data = 'fetch data'
			resolve('')
		}, 2000)
	})
}

function Content() {
	return <>{fetchData()}</>
}
const SuspenseWrapper = () => {
	return (
		<Suspense fallback={<>Loading...</>}>
			<Content />
		</Suspense>
	)
}

页面会先显示fallback内容,然后2s后显示Content内容。

原理解析

从上面的简单demo中可以看出一些Suspense的核心原理:子组件抛出一个Promise(throw new Promise()),Suspense通过错误捕获,检查类型是一个Promise,随后对这个Promise追加一个then方法,在then方法中再次更新Suspense。然后将fallback作为子组件的兄弟节点渲染到页面上,等到Promise状态变化之后,会触发then的执行,再次更新Suspense,展示出子组件的内容,并卸载掉fallback组件

具体流程如下:

  1. 在React执行beginWork进行深度遍历组件生成Fiber树时,当遍历到Suspense组件,待加载的组件作为其子组件,下一个应该遍历该组件。
  2. 当遍历到该待加载的组件时,由于该组件未加载完成会抛出一个异常,该异常为Promise,React捕获到该异常会判断类型,如果是Promise会对其追加一个then方法。在then中再次触发Suspense的更新。React同时还会将下一个遍历的元素重新设为Suspense
  3. 继续beginWork流程,再次遍历到Suspense时,将fallback以及待加载的组件都生成,并直接返回fallback,跳过待加载组件的遍历(因为已经遍历过一遍了)。本次渲染结束之后,页面上会展示fallback的内容。
  4. 当待加载组件加载完成之后,会触发then回调,再次更新Suspense,直接渲染加载后的组件,并卸载fallback组件

一次beginWork流程,遍历的组件顺序如下:

tex
Suspense -> 待加载组件 -> Suspense -> fallback

待加载组件

核心原理就分为两部分:

  1. 加载完成,返回组件
  2. 否则(正在加载以及加载失败),抛出异常,异常内容为一个Promise

异常的捕获

React的reconcil阶段是在workLoop中进行的

js
do {
	try {
		workLoopSync();
		break;
	} catch (thrownValue) {
		handleError(root, thrownValue);
	}
} while (true);

包裹了一层try..catchthrow的异常会被捕获,进入handleError中处理。如果是Promise,会做这些事情:

  1. 获取到最近的Suspense父级组件,没有会报错。
  2. 找到之后,给父级组件打上一些标记,意为需要渲染fallback(二次遍历Suspense)。
  3. 将抛出的Promise添加到Suspense父组件的updateQueue中,后续会对这个Promise进行then的追加
  4. 遍历完之后会进入completeWork的流程,根据Suspense打上的标记,将下一次遍历的节点还设置为Suspense。这一步主要是为了生成fallback组件

添加Promise的then回调

上面说到会将Promise添加到父级SuspenseupdateQueue中,进入到commit阶段会遍历updateQueue,为每一个都添加回调,回调执行时会触发Suspense组件的更新

Suspense

说完了上面几个的原理,Suspense的原理就是将这几个东西串起来的。

首屏渲染(mount):

  1. beginWork遍历到Suspense时,会访问子节点(待加载节点),此时子节点正在加载会抛出异常,捕获之后会将下一次遍历节点还设为Suspense

    react^18.3.1中 是将Suspense放到了子组件最后,即会遍历完所有的子组件,再回到Suspense。但是也有人说 貌似是个bug,等到版本更新之后再验证

  2. 第二次遍历到Suspense时,将fallback节点当做待加载节点的兄弟节点,直接返回fallback节点,避免第二次访问子节点。

子节点加载前的更新:

即其他组件触发的Suspense更新

  1. 第一次遍历到Suspense时,如果fallback组件有值,就将其添加到Suspense组件的待删除队列deletions中。其他和上面一样
  2. 第二次遍历到Suspense时,会清掉待删除队列deletions。其他和上面也是一样的

子节点加载后的更新:

加载完成会触发then回调,再次更新Suspense

  1. 第一次遍历到Suspense时,都是一样的。但是由于已经加载完成,并没有抛出异常,所以不会第二次遍历Suspensefallback组件会被卸载,显示子组件

上一次更新: