Suspense的介绍和原理

库房地址 本节代码分支

系列文章:

  1. React完成系列一 – jsx
  2. 分析React系列二-reconciler
  3. 分析React系列三-打符号
  4. 分析React系列四-commit
  5. 分析React系列五-update流程
  6. 分析React系列六-dispatch update流程
  7. 分析React系列七-事情体系
  8. 分析React系列八-同级节点diff
  9. 分析React系列九-Fragment的部分完成逻辑
  10. 分析React系列十- 调度<合并更新、优先级>
  11. 分析React系列十一- useEffect的完成原理
  12. 分析React系列十二-调度器的完成
  13. 分析React系列十三-react调度
  14. useTransition的完成
  15. useRef的完成
  16. Suspense的介绍和原理(上篇)

Suspense介绍

suspense是React 16.6新出的一个功能,用于异步加载组件,能够让组件在等候异步加载的时分,烘托一些fallback的内容,让用户有更好的体验。

上一章节中,咱们讲解了suspensemount的时分的状况,假如包裹的组件数据未回来之前的一些进程,阅历了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办法

回顾一下之前咱们了解的suspensefiber结构:

  1. suspensechild元素指向Offscreen节点。
  2. Offscreen的节点的子节点是咱们实在的children节点。

Suspense的介绍和原理(下篇)
有了上面的fiber的结构图,咱们再了解updateSuspensePrimaryChildren的作用

  1. Offscreenmode特点符号为visible,烘托正在的节点。
  2. 清理掉正在烘托的fragment包裹的fallbackloading节点。
    • 清理sibling的指向
    • suspanse增加删除符号以及删除的元素
  3. 然后回来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篇,咱们现在全体的流程大概如下:

Suspense的介绍和原理(下篇)

假如咱们点击某一个操作,触发更新的话,会再次展现loading等候数据回来后,才会烘托实在的组件数据。如下图所示:

接下来咱们来评论更新后的履行流程,是如何做到特点值的显现和躲藏的。

触发更新后

假如外部条件发生改变触发更新操作,会先躲藏界面并展现loading,等候数据回来后再次展现界面内容。

全体的流程如下图:

Suspense的介绍和原理(下篇)

  1. 首先由于界面已经有烘托元素,所以会走到update的流程。当烘托到包裹组件的use办法的时分,抛出错误。
  2. unwind到最近的suspense节点,走update挂起流程,展现loading的界面。
  3. 当接口数据回来后,会触发一次新的更新,然后走到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);
    }
  });
}

hideInstanceunhideInstance便是设置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的部分就根本讲完了,下一讲咱们将功能优化方面,比如bailouteagerState等策略。