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
标识,并敞开新的一轮调度
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
分支,用于展现正常的节点烘托。
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
节点。
悉数代码如下所示:
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
函数节点)。进入到函数组件的谐和。
伪代码如下:
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
。
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
的值。
// 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
符号是否有改变。
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
符号
if ((flags & Visibility) !== NoFlags && tag === OffscreenComponent) {
const isHidden = finishedWork.pendingProps.mode === "hidden";
// 处理suspense 的offscreen
hideOrUnhideAllChildren(finishedWork, isHidden);
finishedWork.flags &= ~Visibility;
}
在hideOrUnhideAllChildren
的函数中,咱们需求找到一切的子树的host
节点,然后依据状况处理是躲藏还是显现
/** 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
特点,这样咱们就能够在更新的时分躲藏或显现元素了。
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
等策略。