介绍
Suspense在整个React体系中都非常重要。简单的说,在异步操作(数据获取、模块加载)没有完成的时候显示fallback的内容。结合startTransition
可以在异步操作没有完成前显示旧的内容,避免在fallback、其他内容和新的内容之间切换闪烁。
这里根据一个实践例子和源码深度解析Suspense的实现原理。
举一个例子
jsx
// UserDashboard.js
import React from 'react';
// 假设这里是一个比较大的组件,包含很多逻辑和UI
// 并且为了演示,我们模拟一个延迟加载
function UserDashboard() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
// 模拟数据获取延迟
const timer = setTimeout(() => {
setData("Welcome to your personal dashboard!");
}, 2000); // 模拟 2 秒钟的加载时间
return () => clearTimeout(timer);
}, []);
if (!data) {
// 在这里,如果数据没有准备好,我们不会抛出 Promise,
// 而是显示一个内部加载状态,因为这个组件本身已经加载了。
// 真正的 Suspense 是发生在组件代码加载的时候。
return <div>Loading dashboard content...</div>;
}
return (
<div style={{ border: '1px solid blue', padding: '20px', margin: '20px' }}>
<h2>User Dashboard</h2>
<p>{data}</p>
</div>
);
}
export default UserDashboard;
jsx
// App.js
import React, { Suspense } from 'react';
// 1. 定义一个懒加载组件
// React.lazy 会返回一个组件,这个组件的实际代码会在需要渲染时才加载
const LazyUserDashboard = React.lazy(() => import('./UserDashboard'));
function App() {
const [showDashboard, setShowDashboard] = React.useState(false);
return (
<div style={{ fontFamily: 'Arial', textAlign: 'center' }}>
{/* <h1>My Application</h1> */}
<button onClick={() => setShowDashboard(!showDashboard)}>
{showDashboard ? 'Hide Dashboard' : 'Show Dashboard'}
</button>
{showDashboard && (
// 2. 使用 <Suspense> 包裹懒加载组件
// fallback prop 定义了在懒加载组件代码加载期间显示的内容
<Suspense fallback={
<div style={{ border: '1px dashed gray', padding: '20px', margin: '20px' }}>
Loading User Dashboard module... (This is the Suspense Fallback)
</div>
}>
{/* 3. 渲染懒加载组件 */}
<LazyUserDashboard />
</Suspense>
)}
{/* <p>This is the main application content, always visible.</p> */}
</div>
);
}
export default App;
jsx
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import React from 'react';
createRoot(document.getElementById('root')).render(<App /> )
原理
-
开始渲染Suspense, 但是组件还没有加载好,
updateSuspenseComponent
通过throw
抛出文件加载的pending promise。 -
在错误处理(handleError)中
completeUnitOfWork
,直到Suspense
后停下。再次渲染Suspense
,这次updateSuspenseComponent
会判断到DidCapture
, -
从而进入到
mountSuspenseFallbackChildren
,它会构建Fragment-fallback
子树,再把Fragment
构建为Suspense
的第二个子节点,并返回Fragment
,被赋值给workInProgress
作为beginWork
的下一个节点,这样commit
的时候就会渲染了Fragment-fallback
分支了。完成一次渲染,显示fallback。 -
提交的时候给懒加载模块的promise pending绑定
resolveRetryWakeable
回调。 -
模块加载下来后,触发
resolveRetryWakeable
,再次渲染Suspense
,移除fallback
,渲染OffscreenComponent
。更新显示真实内容。
beginWork unwindWork"] OffscreenComponent["OffscreenComponent
beginWork unwindWork
头节点"] Fragment["Fragment
beginWork completeWork
尾节点"] fallback["fallback
beginWork completeWork
<div style...>Loading...</div>"] LazyComponent["LazyComponent
beginWork unwindWork、throw promise pending
UserDashboard"] SuspenseComponent --> OffscreenComponent SuspenseComponent --> Fragment Fragment --> fallback OffscreenComponent --> LazyComponent
attachSuspenseRetryListeners
为每个 Promise(如懒加载返回的Promise)绑定一个 resolveRetryWakeable 回调。模块下载下来后触发回调再次渲染。
js
function attachSuspenseRetryListeners(finishedWork) {
var wakeables = finishedWork.updateQueue;
if (wakeables !== null) {
...
wakeables.forEach(function (wakeable) {
// Memoize using the boundary fiber to prevent redundant listeners.
var retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); //wakeable是Promise pending
if (!retryCache.has(wakeable)) {
retryCache.add(wakeable);
...
wakeable.then(retry, retry); //给promise pending绑定resolveRetryWakeable
}
});
}
}
function resolveRetryWakeable(boundaryFiber, wakeable) {
...
retryTimedOutBoundary(boundaryFiber, retryLane);
}
function retryTimedOutBoundary(boundaryFiber, retryLane) {
...
var eventTime = requestEventTime();
var root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane);
if (root !== null) {
markRootUpdated(root, retryLane, eventTime);
ensureRootIsScheduled(root, eventTime); //重新渲染
}
}
updateSuspenseComponent
Lazycomponent
抛出一个promise pending后,回退到suspense重新渲染suspense的时候,这次updateSuspenseComponent
判断DidCapture,然后进入条件,构建Fragment-fallback为Suspense
的第二个子节点。
js
function updateSuspenseComponent(){
var suspenseContext = suspenseStackCursor.current;
var showFallback = false;
var didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;//成立
if (didSuspend || shouldRemainOnFallback(suspenseContext, current)) {
showFallback = true; //进入这里
workInProgress.flags &= ~DidCapture;
} else {
...
}
if (showFallback) {
var fallbackFragment = mountSuspenseFallbackChildren(workInProgress, nextPrimaryChildren, nextFallbackChildren, renderLanes);
var primaryChildFragment = workInProgress.child;
primaryChildFragment.memoizedState = mountSuspenseOffscreenState(renderLanes);
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackFragment; //返回Fragment
}
}
返回fallbackFragment
,
协调器会沿着fallbackFragment
这条路径渲染,准备fallback的UI,走Fragment->commitRoot,没有经过Offscreencomponent
,而Offscreencomponent
下方的主子树(UserDashboard
)则会在后台继续工作。
在mountSuspenseFallbackChildren
构建了这样的子树:Fragment和Offscreencomponent是兄弟节点,Offscreencomponent是Suspense的第一个子节点,Fragment是第二个子节点。
第一次没有构建Fragment-fallback子树,在LazyComponent抛出Primose pending的时候,才构建了Fragment-fallback子树,并且
updateSuspenseComponent
和mountSuspenseFallbackChildren
都返回第二个节点 ------ Fragment。
mountSuspenseFallbackChildren
构建Suspense树的结构
js
function mountSuspenseFallbackChildren(workInProgress, primaryChildren, fallbackChildren, renderLanes) {
var mode = workInProgress.mode;
var progressedPrimaryFragment = workInProgress.child; // wip是suspense,child是offscreencomponent,child表示头子节点,目前suspense只有一个子节点,下面会把fallback构建第二个子节点
var primaryChildProps = {
mode: 'hidden',// 把Offscreencomponent fiber的mode改成'hidden'
children: primaryChildren // Lazycomponent的内容
};
var primaryChildFragment; //OffscreenComponent Fiber 结合primaryChildProps,suspense头节点
var fallbackChildFragment;//fallback 的 Fiber fallback内容,suspense第二个节点,被返回
//这是遗留模式 (Legacy Mode) 的分支。如果你用的是 ReactDOM.render() 启动的 React 18 应用,或者更老的 React 17,就可能会进入这个分支。
if ((mode & ConcurrentMode) === NoMode && progressedPrimaryFragment !== null) {
// 创建OffscreenComponent
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childLanes = NoLanes;
primaryChildFragment.pendingProps = primaryChildProps;
...
// 创建Fragment
fallbackChildFragment = createFiberFromFragment(fallbackChildren, mode, renderLanes, null);
} else {//这是并发模式 (Concurrent Mode) 的分支。这是 React 18 中 ReactDOM.createRoot() 启动应用的默认路径。
primaryChildFragment = mountWorkInProgressOffscreenFiber(primaryChildProps, mode);//创建OffscreenComponent
fallbackChildFragment = createFiberFromFragment(fallbackChildren, mode, renderLanes, null);//创建Fragment
}
// 把Fragment构建成Suspense的第二个子节点
primaryChildFragment.return = workInProgress; // OffscreenComponent 的父级是 SuspenseComponent
fallbackChildFragment.return = workInProgress; // Fragment 的父级也是 SuspenseComponent
primaryChildFragment.sibling = fallbackChildFragment; // OffscreenComponent 和 Fragment 是兄弟关系
workInProgress.child = primaryChildFragment; // 将 OffscreenComponent 设置为 SuspenseComponent 的第一个孩子
return fallbackChildFragment; // 实际返回的是 Fragment 节点 而不是头节点OffscreenComponent!!
}
fallbackChildren是fallback的内容,创建Fragment的时候作为Fragment的子节点。
Suspense上有mode属性,值"visible"/"hidden",初始为 "visible"
。
- 这表示 React 默认该子树可以正常渲染并显示。
- 只有当
OffscreenComponent
下的子节点真正"抛出"一个 Promise (pending)导致 Suspense 发生时,OffscreenComponent
的mode
才会被切换为"hidden"
"hidden"
状态是告诉 React:"这个子树虽然我还在后台渲染,但它的内容目前不应该被提交到 DOM,同时,SuspenseComponent
也会被标记为需要显示 fallback
。
最后
可以看到Suspense利用throw Promise<pending>实现,并且构建了Fragment-fallback子树,和懒加载的组件形成了兄弟节点树。