throwException

首先给这个Fiber加上Incompleteside effect,并且晴空effect

主要分为suspenddid error两种情况

suspend

需要符合条件:throw value是一个promise

if (
  value !== null &&
  typeof value === 'object' &&
  typeof value.then === 'function'
)

处理这种情况会有两个循环,在这之前初始化了两个变量earliestTimeoutMsstartTimeMs,都是-1

第一个循环

这个循环主要是计算初始化的两个值。他们只跟Suspense有关系,timeOutAt是在commitWork的时候设置的,所以第一次没有,而设置的值就是当时的currentTime

const currentTime = requestCurrentTime();
finishedWork.stateNode = { timedOutAt: currentTime };

earliestTimeoutMsmaxDuration有关,他会向上找到maxDuration最小的非负数

所以第一次渲染的时候,startTimeMs-1,第二次渲染earliestTimeoutMs-1

第二个循环

首先didTimeout = workInProgress.memoizedState,那么这个什么时候会变成true呢?是在updateSuspense的时候,具体可以看Suspense

我们可以大致模拟一下流程。

  • 经过updateSuspensedidTimeoutfalse
  • 绑定thenable.thenretrySuspendedRoot
  • 如果不是StrictMode会直接去掉Incompleteside effect,不过目前只要是AsyncMode的都会自动机上StrictMode,然后直接return
  • 然后计算absoluteTimeoutMs,然后加上ShouldCaptureside effect
  • 加上ShouldCapture会让Suspense组件再次进入beginWork阶段重新update,这时候didTimeout会变为true,并且渲染fallback的内容
  • 最后

placeholder.png

那么第二个循环再!didTimeout中的逻辑具体是什么呢

首先要绑定suquense组件的resolve之后要执行retrySuspendedRoot,然后计算absoluteTimeoutMs,然后调用renderDidSuspend,这块我们放到下面再讲。

注意

在写这片文章的时候susquense功能并没有正式发布,所以可能会有挺多改动,并且也存在一些 bug,比如maxDuration貌似没用,再比如多次throwException中会给同一个promise绑定多次retry,导致后来retry多次渲染,相信在 17 版本发布的时候应该有所改善。

TODO: 17 版本发布之后再次确认这部分代码

Suspense的情况

这种情况相当于捕捉到了一个错误,这边的操作是向上遍历节点,对HostRootClassComponentClassComponentLazy做一些操作:

  1. 加上shouldCaptureside effect
  2. 创建error update

注意,对于ClassComponent只有当组件声明getDerivedStateFromCatch或者componentDidCatch的组件会做这些操作

function throwException(
    root: FiberRoot,
    returnFiber: Fiber,
    sourceFiber: Fiber,
    value: mixed,
    renderExpirationTime: ExpirationTime
) {
    // The source fiber did not complete.
    sourceFiber.effectTag |= Incomplete;
    // Its effect list is no longer valid.
    sourceFiber.firstEffect = sourceFiber.lastEffect = null;

    if (
        value !== null &&
        typeof value === "object" &&
        typeof value.then === "function"
    ) {
        // This is a thenable.
        const thenable: Thenable = (value: any);

        let workInProgress = returnFiber;
        let earliestTimeoutMs = -1;
        let startTimeMs = -1;
        do {
            if (workInProgress.tag === SuspenseComponent) {
                const current = workInProgress.alternate;
                if (current !== null) {
                    const currentState: SuspenseState | null =
                        current.memoizedState;
                    if (currentState !== null && currentState.didTimeout) {
                        // Reached a boundary that already timed out. Do not search
                        // any further.
                        const timedOutAt = currentState.timedOutAt;
                        startTimeMs = expirationTimeToMs(timedOutAt);
                        // Do not search any further.
                        break;
                    }
                }
                let timeoutPropMs = workInProgress.pendingProps.maxDuration;
                if (typeof timeoutPropMs === "number") {
                    if (timeoutPropMs <= 0) {
                        earliestTimeoutMs = 0;
                    } else if (
                        earliestTimeoutMs === -1 ||
                        timeoutPropMs < earliestTimeoutMs
                    ) {
                        earliestTimeoutMs = timeoutPropMs;
                    }
                }
            }
            workInProgress = workInProgress.return;
        } while (workInProgress !== null);

        // Schedule the nearest Suspense to re-render the timed out view.
        workInProgress = returnFiber;
        do {
            if (
                workInProgress.tag === SuspenseComponent &&
                shouldCaptureSuspense(workInProgress.alternate, workInProgress)
            ) {
                const pingTime =
                    (workInProgress.mode & ConcurrentMode) === NoEffect
                        ? Sync
                        : renderExpirationTime;

                // Attach a listener to the promise to "ping" the root and retry.
                let onResolveOrReject = retrySuspendedRoot.bind(
                    null,
                    root,
                    workInProgress,
                    sourceFiber,
                    pingTime
                );
                if (enableSchedulerTracing) {
                    onResolveOrReject =
                        Schedule_tracing_wrap(onResolveOrReject);
                }
                thenable.then(onResolveOrReject, onResolveOrReject);
                if ((workInProgress.mode & ConcurrentMode) === NoEffect) {
                    workInProgress.effectTag |= CallbackEffect;

                    // Unmount the source fiber's children
                    const nextChildren = null;
                    reconcileChildren(
                        sourceFiber.alternate,
                        sourceFiber,
                        nextChildren,
                        renderExpirationTime
                    );
                    sourceFiber.effectTag &= ~Incomplete;

                    if (sourceFiber.tag === ClassComponent) {
                        sourceFiber.effectTag &= ~LifecycleEffectMask;
                        const current = sourceFiber.alternate;
                        if (current === null) {
                            sourceFiber.tag = IncompleteClassComponent;
                        }
                    }

                    // Exit without suspending.
                    return;
                }

                let absoluteTimeoutMs;
                if (earliestTimeoutMs === -1) {
                    absoluteTimeoutMs = maxSigned31BitInt;
                } else {
                    if (startTimeMs === -1) {
                        const earliestExpirationTime =
                            findEarliestOutstandingPriorityLevel(
                                root,
                                renderExpirationTime
                            );
                        const earliestExpirationTimeMs = expirationTimeToMs(
                            earliestExpirationTime
                        );
                        startTimeMs =
                            earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
                    }
                    absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
                }
                renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);

                workInProgress.effectTag |= ShouldCapture;
                workInProgress.expirationTime = renderExpirationTime;
                return;
            }
            workInProgress = workInProgress.return;
        } while (workInProgress !== null);
        // No boundary was found. Fallthrough to error mode.
        value = new Error(
            "An update was suspended, but no placeholder UI was provided."
        );
    }

    renderDidError();
    value = createCapturedValue(value, sourceFiber);
    let workInProgress = returnFiber;
    do {
        switch (workInProgress.tag) {
            case HostRoot: {
                const errorInfo = value;
                workInProgress.effectTag |= ShouldCapture;
                workInProgress.expirationTime = renderExpirationTime;
                const update = createRootErrorUpdate(
                    workInProgress,
                    errorInfo,
                    renderExpirationTime
                );
                enqueueCapturedUpdate(workInProgress, update);
                return;
            }
            case ClassComponent:
                // Capture and retry
                const errorInfo = value;
                const ctor = workInProgress.type;
                const instance = workInProgress.stateNode;
                if (
                    (workInProgress.effectTag & DidCapture) === NoEffect &&
                    (typeof ctor.getDerivedStateFromError === "function" ||
                        (instance !== null &&
                            typeof instance.componentDidCatch === "function" &&
                            !isAlreadyFailedLegacyErrorBoundary(instance)))
                ) {
                    workInProgress.effectTag |= ShouldCapture;
                    workInProgress.expirationTime = renderExpirationTime;
                    // Schedule the error boundary to re-render using updated state
                    const update = createClassErrorUpdate(
                        workInProgress,
                        errorInfo,
                        renderExpirationTime
                    );
                    enqueueCapturedUpdate(workInProgress, update);
                    return;
                }
                break;
            default:
                break;
        }
        workInProgress = workInProgress.return;
    } while (workInProgress !== null);
}

absoluteTimeoutMs

我们上面提到了:

  • earliestTimeoutMs是最小的maxDuration
  • startTimeMs是上次commit设置的timedOutAt

如果没有设置maxDuration,那么absoluteTimeoutMsmaxSigned31BitInt也就是1073741823,基本意思就是永远不过超时

如果有设置maxDuration,因为第一次没有startTimeMs,所以要计算一个,从当前root的所有过期时间中找一个优先级最高的,然后减去LOW_PRIORITY_EXPIRATION,这部分计算不必要太在意,我们主要看一下第二次进来的时候

在有earliestTimeoutMsstartTimeMs的情况下,absoluteTimeoutMs等于startTimeMs + earliestTimeoutM,简单来说就是少了第一次渲染到这次有更新进来中间的空白时间,也符合maxDuration需要的含义。

但是根据之前说的逻辑,在有startTimeMs的情况下根本不会执行earliestTimeoutMs的逻辑,所以这个逻辑是否有问题?实际我写了个小例子看了一下结果也是和我的预期一样

TODO: 17 版本来验证这个逻辑是否有问题

回到renderRoot

// throwException
let absoluteTimeoutMs;
if (earliestTimeoutMs === -1) {
    absoluteTimeoutMs = maxSigned31BitInt;
} else {
    if (startTimeMs === -1) {
        const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
            root,
            renderExpirationTime
        );
        const earliestExpirationTimeMs = expirationTimeToMs(
            earliestExpirationTime
        );
        startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
    }
    absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
}

renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);

// renderDidSuspend
function renderDidSuspend(
    root: FiberRoot,
    absoluteTimeoutMs: number,
    suspendedTime: ExpirationTime
) {
    // Schedule the timeout.
    if (
        absoluteTimeoutMs >= 0 &&
        nextLatestAbsoluteTimeoutMs < absoluteTimeoutMs
    ) {
        nextLatestAbsoluteTimeoutMs = absoluteTimeoutMs;
    }
}

results matching ""

    No results matching ""

    Jokcy的二维码

    扫码添加Jokcy,更多更新更优质的前端学习内容不断更新中,期待与你一起成长!

    Jokcy的二维码