Suspense的介绍和原理
系列文章:
- React实现系列一 - jsx
- 剖析React系列二-reconciler
- 剖析React系列三-打标记
- 剖析React系列四-commit
- 剖析React系列五-update流程
- 剖析React系列六-dispatch update流程
- 剖析React系列七-事件系统
- 剖析React系列八-同级节点diff
- 剖析React系列九-Fragment的部分实现逻辑
- 剖析React系列十- 调度<合并更新、优先级>
- 剖析React系列十一- useEffect的实现原理
- 剖析React系列十二-调度器的实现
- 剖析React系列十三-react调度
- useTransition的实现
- useRef的实现
- Suspense的介绍和原理(上篇)
Suspense介绍
suspense是React 16.6
新出的一个功能,用于异步加载组件,可以让组件在等待异步加载的时候,渲染一些fallback的内容,让用户有更好的体验。
上一章节中,我们讲解了suspense
的mount
的时候的情况,如果包裹的组件数据未返回之前的一些步骤,经历了mount
阶段的mountSuspensePrimaryChildren
正常流程 和 mountSuspenseFallbackChildren
挂起流程,可以回顾一下上一篇文章Suspense的介绍和原理(上篇)
这一章节我们讲解当我们监听到数据返回重新渲染的逻辑以及触发更新操作的情况。
attachPingListener
我们在上一章中### 请求返回后触发更新
的小结中提到,如果返回数据后需要ping
一下告诉程序数据请求回来。
在attachPingListener
中新增优先级lane
标识,并开启新的一轮调度
javascript
function attachPingListener(
root: FiberRootNode,
wakeable: Wakeable<any>,
lane: Lane
) {
function ping() {
// fiberRootNode
markRootPinged(root, lane);
markRootUpdated(root, lane);
ensureRootIsScheduled(root); // 开启新的调度
}
wakeable.then(ping, ping);
}
}
从根节点开始调度,当调和到suspense
的时候,执行updateSuspenseComponent
方法,由于此时界面上已经展示了loading
节点。所以wip.alternate
节点此时不为null
,同时由于之前是挂起状态,清除DidCapture
标记,再次进入的时候didSuspend
的值为false
。
所以会走到如下这个分支updateSuspensePrimaryChildren
分支,用于展示正常的节点渲染。
JavaScript
function updateSuspenseComponent(wip: FiberNode) {
const current = wip.alternate;
const nextProps = wip.pendingProps;
let showFallback = false; // 是否显示fallback
const didSuspend = (wip.flags & DidCapture) !== NoFlags; // 是否挂起
if (didSuspend) {
// 显示fallback
showFallback = true;
wip.flags &= ~DidCapture; // 清除DidCapture
}
const nextPrimaryChildren = nextProps.children; // 主渲染的内容
const nextFallbackChildren = nextProps.fallback;
pushSuspenseHandler(wip);
if (current === null) {
// mount
if (showFallback) {
// 挂起
return mountSuspenseFallbackChildren(
wip,
nextPrimaryChildren,
nextFallbackChildren
);
} else {
// 正常
return mountSuspensePrimaryChildren(wip, nextPrimaryChildren);
}
} else {
// update
if (showFallback) {
// 挂起
return updateSuspenseFallbackChildren(
wip,
nextPrimaryChildren,
nextFallbackChildren
);
} else {
// 正常
return updateSuspensePrimaryChildren(wip, nextPrimaryChildren);
}
}
}
updateSuspensePrimaryChildren方法
回顾一下之前我们了解的suspense
的fiber
结构:
suspense
的child
元素指向Offscreen
节点。Offscreen
的节点的子节点是我们真正的children
节点。
有了上面的fiber
的结构图,我们再理解updateSuspensePrimaryChildren
的作用
- 将
Offscreen
的mode
属性标记为visible
,渲染正在的节点。 - 清理掉正在渲染的
fragment
包裹的fallback
loading节点。- 清理
sibling
的指向 suspanse
添加删除标记以及删除的元素
- 清理
- 然后返回
Offscreen
对应的fiber
节点。
全部代码如下所示:
javascript
function updateSuspensePrimaryChildren(wip, primaryChildren) {
const current = wip.alternate;
const currentPrimaryChildFragment = current.child;
const currentFallbackChildFragment = currentPrimaryChildFragment.sibling;
const primaryChildProps = {
mode: "visible",
children: primaryChildren
};
const primaryChildFragment = createWorkInProgress(currentPrimaryChildFragment, primaryChildProps);
primaryChildFragment.return = wip;
primaryChildFragment.sibling = null;
wip.child = primaryChildFragment;
if (currentFallbackChildFragment) {
const deletions = wip.deletions;
if (deletions === null) {
wip.deletions = [currentFallbackChildFragment];
wip.flags |= ChildDeletion;
} else {
deletions.push(currentFallbackChildFragment);
}
}
return primaryChildFragment;
}
继续调和
返回Offscreen
对应的fiber
节点后,继续beginWork
的调和阶段。进入到updateOffscreenComponent
的执行。正常的调和流程,然后到达我们例子中的真正的子节点渲染(Cpn
函数节点)。进入到函数组件的调和。
伪代码如下:
javascript
case OffscreenComponent:
return updateOffscreenComponent(wip);
function updateOffscreenComponent(wip) {
const nextProps = wip.pendingProps;
const nextChildren = nextProps.children;
reconcileChildren(wip, nextChildren);
return wip.child;
}
包裹的函数组件调和
再次进入Cpn
组件的时候,我们会再次的执行到use
这个hooks。但是此时fetchData
这个promise
的状态已经不再是pending
了,转换成了fulfilled
。
JavaScript
export function Cpn({ id, timeout }) {
const [num, updateNum] = useState(0);
const { data } = use(fetchData(id, timeout));
if (num !== 0 && num % 5 === 0) {
cachePool[id] = null;
}
useEffect(() => {
console.log("effect create");
return () => console.log("effect destroy");
}, []);
return (
<ul onClick={() => updateNum(num + 1)}>
<li>ID: {id}</li>
<li>随机数: {data}</li>
<li>状态: {num}</li>
</ul>
);
}
当进入use
的实现逻辑后,会执行到trackUsedThenable
,由于收到的是fulfilled
状态,会直接返回对应的value
的值。
javascript
// use hooks的实现
function use(usable) {
if (usable !== null && typeof usable === "object") {
if (typeof usable.then === "function") {
const thenable = usable;
return trackUsedThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context = usable;
return readContext(context);
}
}
throw new Error("不支持的use参数");
}
export function trackUsedThenable(thenable) {
switch (thenable.status) {
case "fulfilled":
return thenable.value;
case "rejected":
throw thenable.reason;
default:
if (typeof thenable.status === "string") {
thenable.then(noop, noop);
} else {
const pending = thenable;
pending.status = "pending";
pending.then((val)=>{
if (pending.status === "pending") {
const fulfilled = pending;
fulfilled.status = "fulfilled";
fulfilled.value = val;
}
}
, (err)=>{
const rejected = pending;
rejected.status = "rejected";
rejected.reason = err;
}
);
}
break;
}
suspendedThenable = thenable;
throw SuspenseException;
}
这样use
就可以拿到真实返回的值,然后在子组件的调和过程中进行使用。
自此,suspense
初始化显示loading
,以及得到数据后展示真实的数据的过程就完成了。
结合上下2篇,我们目前整体的流程大概如下:
如果我们点击某一个操作,触发更新的话,会再次展示loading
等待数据返回后,才会渲染真实的组件数据。如下图所示:
接下来我们来讨论更新后的执行流程,是如何做到属性值的显示和隐藏的。
触发更新后
如果外部条件发生变化触发更新操作,会先隐藏界面并展示loading
,等待数据返回后再次展示界面内容。
整体的流程如下图:
- 首先由于界面已经有渲染元素,所以会走到
update
的流程。当渲染到包裹组件的use
方法的时候,抛出错误。 unwind
到最近的suspense
节点,走update
的挂起
流程,展示loading
的界面。- 当接口数据返回后,会触发一次新的更新,然后走到
update
的正常流程,渲染数据
这里个地方需要注意,这也是在更新的时候隐藏和显示
的判断依据,在update
挂起流程的时候,mode
的值被标记为hidden
,但是在正常流程mode
值为visible
隐藏和显示的切换
回归阶段打标记
由于mode
值在挂起和正常渲染的时候的不同,我们在向上递归的时候,可以根据前后对比,进行flag
标记是否有变化。
javascript
export const completeWork = (wip: FiberNode) => {
/**
* 对比Offscreen的mode(hide/visibity) 需要再suspense中
* 因为如果在OffscreenComponent中比较的话,当在Fragment分支的时候
* completeWork并不会走到OffscreenComponent
*
* current Offscreen mode 和 wip Offscreen mode 的对比
*/
// 比较变化mode的变化(visible | hide)
const offscreenFiber = wip.child as FiberNode;
const isHidden = offscreenFiber.pendingProps.mode === "hidden";
const currentOffscreenFiber = offscreenFiber.alternate;
if (currentOffscreenFiber !== null) {
// update
const wasHidden = currentOffscreenFiber.pendingProps.mode === "hidden";
if (wasHidden !== isHidden) {
// 可见性发生了变化
offscreenFiber.flags |= Visibility;
bubbleProperties(offscreenFiber);
}
} else if (isHidden) {
// mount 并且 hidden的状态 todo: 这里什么流程走到
offscreenFiber.flags |= Visibility;
bubbleProperties(offscreenFiber);
}
bubbleProperties(wip);
return null;
}
如果前后2次的对比值不同的话 ,就添加Visibility
标记,用于commit
阶段去判断是否展示内容。
commit
阶段根据标记处理渲染
在对每一个fiber
进行处理的过程中,判断是否是OffscreenComponent
并且有Visibility
标记
javascript
if ((flags & Visibility) !== NoFlags && tag === OffscreenComponent) {
const isHidden = finishedWork.pendingProps.mode === "hidden";
// 处理suspense 的offscreen
hideOrUnhideAllChildren(finishedWork, isHidden);
finishedWork.flags &= ~Visibility;
}
在hideOrUnhideAllChildren
的函数中,我们需要找到所有的子树的host
节点,然后根据状态处理是隐藏还是显示
JavaScript
/** OffscreenComponent中的子host 处理,可能是一个或者多个
function Cpn() {
return (
<p>123</p>
)
}
情况1,一个host节点:
<Suspense fallback={<div>loading...</div>}>
<Cpn/>
</Suspense>
情况2,多个host节点:
<Suspense fallback={<div>loading...</div>}>
<Cpn/>
<div>
<p>你好</p>
</div>
</Suspense>
*/
function hideOrUnhideAllChildren(finishedWork: FiberNode, isHidden: boolean) {
//1. 找到所有子树的顶层host节点
findHostSubtreeRoot(finishedWork, (hostRoot) => {
//2. 标记隐藏或者展示
const instance = hostRoot.stateNode;
if (hostRoot.tag === HostComponent) {
isHidden ? hideInstance(instance) : unhideInstance(instance);
} else if (hostRoot.tag === HostText) {
isHidden
? hideTextInstance(instance)
: unhideTextInstance(instance, hostRoot.memoizedProps.content);
}
});
}
hideInstance
和unhideInstance
就是设置host
节点的display
属性,这样我们就可以在更新的时候隐藏或显示元素了。
JavaScript
export function hideInstance(instance: Instance) {
const style = (instance as HTMLElement).style;
style.setProperty("display", "none", "important");
}
export function unhideInstance(instance: Instance) {
const style = (instance as HTMLElement).style;
style.display = "";
}
至此我们的suspense
的部分就基本讲完了,下一讲我们将性能优化方面,比如bailout
和eagerState
等策略。