// TODO: computeExpirationForFiber also reads the priority. Pass the // priority as an argument to that function and this one. const priorityLevel = getCurrentPriorityLevel(); // 首次渲染 NormalPriority = 3
if (expirationTime === Sync) { // 首次渲染等于Sync // 首次渲染过程中,处理过executionContext的地方位于unbatchedUpdates中 // 经过如下步骤: // const prevExecutionContext = executionContext; // executionContext &= ~BatchedContext; // executionContext |= LegacyUnbatchedContext; // 此时的executionContext中加入了LegacyUnbatchedContext,所以值为0b0001000,也就是8 if ( // Check if we're inside unbatchedUpdates (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering (executionContext & (RenderContext | CommitContext)) === NoContext ) { // 首次渲染进入该分支 // Register pending interactions on the root to avoid losing traced interaction data. schedulePendingInteractions(root, expirationTime);
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed // root inside of batchedUpdates should be synchronous, but layout updates // should be deferred until the end of the batch. performSyncWorkOnRoot(root); } else { ensureRootIsScheduled(root); schedulePendingInteractions(root, expirationTime); if (executionContext === NoContext) { // Flush the synchronous work now, unless we're already working or inside // a batch. This is intentionally inside scheduleUpdateOnFiber instead of // scheduleCallbackForFiber to preserve the ability to schedule a callback // without immediately flushing it. We only do this for user-initiated // updates, to preserve historical behavior of legacy mode. flushSyncCallbackQueue(); } } } else { // Schedule a discrete update but only if it's not Sync. if ( (executionContext & DiscreteEventContext) !== NoContext && // Only updates at user-blocking priority or greater are considered // discrete, even inside a discrete event. (priorityLevel === UserBlockingPriority || priorityLevel === ImmediatePriority) ) { // This is the result of a discrete event. Track the lowest priority // discrete update per root so we can flush them early, if needed. if (rootsWithPendingDiscreteUpdates === null) { rootsWithPendingDiscreteUpdates = newMap([[root, expirationTime]]); } else { const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); if ( lastDiscreteTime === undefined || lastDiscreteTime > expirationTime ) { rootsWithPendingDiscreteUpdates.set(root, expirationTime); } } } // Schedule other updates after in case the callback is sync. ensureRootIsScheduled(root); schedulePendingInteractions(root, expirationTime); } }
checkForNestedUpdates
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
const NESTED_UPDATE_LIMIT = 50; let nestedUpdateCount: number = 0;
functioncheckForNestedUpdates() { if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { nestedUpdateCount = 0; rootWithNestedUpdates = null; invariant( false, 'Maximum update depth exceeded. This can happen when a component ' + 'repeatedly calls setState inside componentWillUpdate or ' + 'componentDidUpdate. React limits the number of nested updates to ' + 'prevent infinite loops.', ); } }
// This is split into a separate function so we can mark a fiber with pending // work without treating it as a typical update that originates from an event; // e.g. retrying a Suspense boundary isn't an update, but it does schedule work // on a fiber. functionmarkUpdateTimeFromFiberToRoot(fiber, expirationTime) { // Update the source fiber's expiration time if (fiber.expirationTime < expirationTime) { fiber.expirationTime = expirationTime; } let alternate = fiber.alternate; if (alternate !== null && alternate.expirationTime < expirationTime) { // 首次渲染alternate是null,不会进入这个分支 alternate.expirationTime = expirationTime; }
// Walk the parent path to the root and update the child expiration time. let node = fiber.return; // 首次渲染,值为null let root = null; if (node === null && fiber.tag === HostRoot) { // 首次渲染进入该分支 root = fiber.stateNode; // 首次渲染值为FiberRoot } else { while (node !== null) { alternate = node.alternate; if (node.childExpirationTime < expirationTime) { node.childExpirationTime = expirationTime; if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; } } elseif ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; } if (node.return === null && node.tag === HostRoot) { root = node.stateNode; break; } node = node.return; } }
if (root !== null) { if (workInProgressRoot === root) { // 首次渲染 workInProgressRoot === null,不进入该分支 // Received an update to a tree that's in the middle of rendering. Mark // that's unprocessed work on this root. markUnprocessedUpdateTime(expirationTime);
if (workInProgressRootExitStatus === RootSuspendedWithDelay) { // The root already suspended with a delay, which means this render // definitely won't finish. Since we have a new update, let's mark it as // suspended now, right before marking the incoming update. This has the // effect of interrupting the current render and switching to the update. // TODO: This happens to work when receiving an update during the render // phase, because of the trick inside computeExpirationForFiber to // subtract 1 from `renderExpirationTime` to move it into a // separate bucket. But we should probably model it with an exception, // using the same mechanism we use to force hydration of a subtree. // TODO: This does not account for low pri updates that were already // scheduled before the root started rendering. Need to track the next // pending expiration time (perhaps by backtracking the return path) and // then trigger a restart in the `renderDidSuspendDelayIfPossible` path. markRootSuspendedAtTime(root, renderExpirationTime); } } // Mark that the root has a pending update. markRootUpdatedAtTime(root, expirationTime); }
// Update the range of suspended times. Treat everything lower priority or // equal to this update as unsuspended. const firstSuspendedTime = root.firstSuspendedTime; // 首次渲染 NoWork = 0 if (firstSuspendedTime !== NoWork) { // 首次渲染不会进入该分支 if (expirationTime >= firstSuspendedTime) { // The entire suspended range is now unsuspended. root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork; } elseif (expirationTime >= root.lastSuspendedTime) { root.lastSuspendedTime = expirationTime + 1; }
// This is a pending level. Check if it's higher priority than the next // known pending level. if (expirationTime > root.nextKnownPendingLevel) { root.nextKnownPendingLevel = expirationTime; } } }
// ImmediatePriority as unstable_ImmediatePriority, // UserBlockingPriority as unstable_UserBlockingPriority, // NormalPriority as unstable_NormalPriority, // IdlePriority as unstable_IdlePriority, // LowPriority as unstable_LowPriority,
functiongetCurrentPriorityLevel(): ReactPriorityLevel{ switch (Scheduler_getCurrentPriorityLevel()) { case Scheduler_ImmediatePriority: return ImmediatePriority; case Scheduler_UserBlockingPriority: return UserBlockingPriority; case Scheduler_NormalPriority: return NormalPriority; case Scheduler_LowPriority: return LowPriority; case Scheduler_IdlePriority: return IdlePriority; default: invariant(false, 'Unknown priority level.'); } }
首次渲染返回的结果是NormalPriority
schedulePendingInteractions
1 2 3 4 5 6 7 8 9
functionschedulePendingInteractions(root, expirationTime) { // This is called when work is scheduled on a root. // It associates the current interactions with the newly-scheduled expiration. // They will be restored when that expiration is later committed. if (!enableSchedulerTracing) { // enableSchedulerTracing = true return; } scheduleInteractions(root, expirationTime, __interactionsRef.current); // __interactionsRef.current = Set[] }
// This is the entry point for synchronous tasks that don't go // through Scheduler functionperformSynliuchengcWorkOnRoot(root) { invariant( (executionContext & (RenderContext | CommitContext)) === NoContext, 'Should not already be working.', );
let expirationTime; if (lastExpiredTime !== NoWork) { // There's expired work on this root. Check if we have a partial tree // that we can reuse. if ( root === workInProgressRoot && renderExpirationTime >= lastExpiredTime ) { // There's a partial tree with equal or greater than priority than the // expired level. Finish rendering it before rendering the rest of the // expired work. expirationTime = renderExpirationTime; } else { // Start a fresh tree. expirationTime = lastExpiredTime; } } else { // There's no expired work. This must be a new, synchronous render. expirationTime = Sync; // 首次渲染 MAX_SIGNED_31_BIT_INT 1073741823 }
let exitStatus = renderRootSync(root, expirationTime);
// If an error occurred during hydration, // discard server response and fall back to client side render. if (root.hydrate) { root.hydrate = false; clearContainer(root.containerInfo); }
// If something threw an error, try rendering one more time. We'll // render synchronously to block concurrent data mutations, and we'll // render at Idle (or lower) so that all pending updates are included. // If it still fails after the second attempt, we'll give up and commit // the resulting tree. expirationTime = expirationTime > Idle ? Idle : expirationTime; exitStatus = renderRootSync(root, expirationTime); }
// We now have a consistent tree. Because this is a sync render, we // will commit it even if something suspended. const finishedWork: Fiber = (root.current.alternate: any); root.finishedWork = finishedWork; root.finishedExpirationTime = expirationTime; root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork); commitRoot(root);
// Before exiting, make sure there's a callback scheduled for the next // pending level. ensureRootIsScheduled(root);
if (workInProgress !== null) { // This is a sync render, so we should have finished the whole tree. invariant( false, 'Cannot commit an incomplete root. This error is likely caused by a ' + 'bug in React. Please file an issue.', ); }
// Set this to null to indicate there's no in-progress render. workInProgressRoot = null;
functionpushDispatcher(root) { const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = ContextOnlyDispatcher; if (prevDispatcher === null) { // The React isomorphic package does not include a default dispatcher. // Instead the first renderer will lazily attach one, in order to give // nicer error messages. return ContextOnlyDispatcher; // 这里返回ContextOnlyDispatcher(感觉像是和Hook相关的) } else { return prevDispatcher; } }
// // The root we're working on // let workInProgressRoot: FiberRoot | null = null;
// // The fiber we're working on // let workInProgress: Fiber | null = null;
// const RootIncomplete = 0; // // Whether to root completed, errored, suspended, etc. // let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
// // A fatal error, if one is thrown // let workInProgressRootFatalError: mixed = null;
// // Most recent event time among processed updates during this render. // // This is conceptually a time stamp but expressed in terms of an ExpirationTime // // because we deal mostly with expiration times in the hot path, so this avoids // // the conversion happening in the hot path. // let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync; // let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync; // let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
// // The work left over by components that were visited during this render. Only // // includes unprocessed updates, not work in bailed out children. // let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
// // If we're pinged while rendering we don't always restart immediately. // // This flag determines if it might be worthwhile to restart if an opportunity // // happens latere. // let workInProgressRootHasPendingPing: boolean = false;
// // Marks the need to reschedule pending interactions at these expiration times // // during the commit phase. This enables them to be traced across components // // that spawn new work during render. E.g. hidden boundaries, suspended SSR // // hydration or SuspenseList. // let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
const timeoutHandle = root.timeoutHandle; // 首次渲染时 -1 if (timeoutHandle !== noTimeout) { // 首次渲染不进入该分支 // The root previous suspended and scheduled a timeout to commit a fallback // state. Now that we have additional work, cancel the timeout. root.timeoutHandle = noTimeout; // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above cancelTimeout(timeoutHandle); }
// Check if there's a suspended level at lower priority. const lastSuspendedTime = root.lastSuspendedTime; // 0 if (lastSuspendedTime !== NoWork && lastSuspendedTime < expirationTime) { // 首次渲染不进入该分支 const lastPingedTime = root.lastPingedTime; // Make sure the suspended level is marked as pinged so that we return back // to it later, in case the render we're about to start gets aborted. // Generally we only reach this path via a ping, but we shouldn't assume // that will always be the case. // Note: This is defensive coding to prevent a pending commit from // being dropped without being rescheduled. It shouldn't be necessary. if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) { root.lastPingedTime = lastSuspendedTime; } }
// 此时的current { actualDuration: 0, actualStartTime: -1, alternate: null, child: null, childExpirationTime: NoWork, // 0 dependencies_old: null, effectTag: NoEffect, // 0 elementType: null, expirationTime: NoWork, // 0 firstEffect: null, index: 0, key: null, lastEffect: null, memoizedProps: null, memoizedState: null, mode: NoMode, // 0 nextEffect: null, pendingProps: null, ref: null, return: null, setBaseDuration: 0, sibling: null, stateNode: container, tag: HostRoot, // 3 treeBaseDuration: 0, type: null, updateQueue: { baseState: null, effects: null, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null, }, }, } // This is used to create an alternate fiber to do work on. exportfunctioncreateWorkInProgress(current: Fiber, pendingProps: any): Fiber{ let workInProgress = current.alternate; // 首次渲染 null if (workInProgress === null) { // We use a double buffering pooling technique because we know that we'll // only ever need at most two versions of a tree. We pool the "other" unused // node that we're free to reuse. This is lazily created to avoid allocating // extra objects for things that are never updated. It also allow us to // reclaim the extra memory if needed. workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; // 上面的操作相当于半深拷贝了一个root对应的Fiber为workInProgress // 然后通过Fiber的alternate属性将current和workInProgress两个Fiber相互引用 workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type. workInProgress.type = current.type;
// We already have an alternate. // Reset the effect tag. workInProgress.effectTag = NoEffect;
// The effect list is no longer valid. workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null;
if (enableProfilerTimer) { // We intentionally reset, rather than copy, actualDuration & actualStartTime. // This prevents time from endlessly accumulating in new commits. // This has the downside of resetting values for different priority renders, // But works for yielding (the common case) and should support resuming. workInProgress.actualDuration = 0; workInProgress.actualStartTime = -1; } }
// Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies_old; workInProgress.dependencies_old = currentDependencies === null ? null : { expirationTime: currentDependencies.expirationTime, firstContext: currentDependencies.firstContext, responders: currentDependencies.responders, };
// These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref;
if (enableProfilerTimer) { workInProgress.selfBaseDuration = current.selfBaseDuration; workInProgress.treeBaseDuration = current.treeBaseDuration; }
return workInProgress; }
schedulePendingInteractions
1 2 3 4 5 6 7 8 9
functionschedulePendingInteractions(root, expirationTime) { // This is called when work is scheduled on a root. // It associates the current interactions with the newly-scheduled expiration. // They will be restored when that expiration is later committed. if (!enableSchedulerTracing) { return; } scheduleInteractions(root, expirationTime, __interactionsRef.current); }
// The work loop is an extremely hot path. Tell Closure not to inline it.
functionworkLoopSync() { // Already timed out, so perform work without checking if we need to yield. while (workInProgress !== null) { performUnitOfWork(workInProgress); } }
functionperformUnitOfWork(unitOfWork: Fiber): void{ // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = unitOfWork.alternate;
let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); next = beginWork(current, unitOfWork, renderExpirationTime); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { // 首次渲染进入该分支 next = beginWork(current, unitOfWork, renderExpirationTime); }
unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { workInProgress = next; }
if ( oldProps !== newProps || hasLegacyContextChanged() || // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { // If props or context changed, mark the fiber as having performed work. // This may be unset if the props are determined to be equal later (memo). didReceiveUpdate = true; } elseif (updateExpirationTime < renderExpirationTime) { // updateExpirationTime = renderExpirationTime = 1073741823 didReceiveUpdate = false; // This fiber does not have any pending work. Bailout without entering // the begin phase. There's still some bookkeeping we that needs to be done // in this optimized path, mostly pushing stuff onto the stack. switch (workInProgress.tag) { case HostRoot: pushHostRootContext(workInProgress); resetHydrationState(); break; case HostComponent: pushHostContext(workInProgress); if ( workInProgress.mode & ConcurrentMode && renderExpirationTime !== Never && shouldDeprioritizeSubtree(workInProgress.type, newProps) ) { if (enableSchedulerTracing) { markSpawnedWork(Never); } // Schedule this fiber to re-render at offscreen priority. Then bailout. workInProgress.expirationTime = workInProgress.childExpirationTime = Never; returnnull; } break; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { pushLegacyContextProvider(workInProgress); } break; } case HostPortal: pushHostContainer( workInProgress, workInProgress.stateNode.containerInfo, ); break; case ContextProvider: { const newValue = workInProgress.memoizedProps.value; pushProvider(workInProgress, newValue); break; } case Profiler: if (enableProfilerTimer) { // Profiler should only call onRender when one of its descendants actually rendered. const hasChildWork = workInProgress.childExpirationTime >= renderExpirationTime; if (hasChildWork) { workInProgress.effectTag |= Update; }
// Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; stateNode.effectDuration = 0; stateNode.passiveEffectDuration = 0; } break; case SuspenseComponent: { const state: SuspenseState | null = workInProgress.memoizedState; if (state !== null) { if (enableSuspenseServerRenderer) { if (state.dehydrated !== null) { pushSuspenseContext( workInProgress, setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); // We know that this component will suspend again because if it has // been unsuspended it has committed as a resolved Suspense component. // If it needs to be retried, it should have work scheduled on it. workInProgress.effectTag |= DidCapture;
returnnull; } }
// If this boundary is currently timed out, we need to decide // whether to retry the primary children, or to skip over it and // go straight to the fallback. Check the priority of the primary // child fragment. const primaryChildFragment: Fiber = (workInProgress.child: any); const primaryChildExpirationTime = primaryChildFragment.childExpirationTime; if (primaryChildExpirationTime >= renderExpirationTime) { // The primary children have pending work. Use the normal path // to attempt to render the primary children again. return updateSuspenseComponent( current, workInProgress, renderExpirationTime, ); } else { // The primary child fragment does not have pending work marked // on it...
// ...usually. There's an unfortunate edge case where the fragment // fiber is not part of the return path of the children, so when // an update happens, the fragment doesn't get marked during // setState. This is something we should consider addressing when // we refactor the Fiber data structure. (There's a test with more // details; to find it, comment out the following block and see // which one fails.) // // As a workaround, we need to recompute the `childExpirationTime` // by bubbling it up from the next level of children. This is // based on similar logic in `resetChildExpirationTime`. let primaryChild = primaryChildFragment.child; while (primaryChild !== null) { const childUpdateExpirationTime = primaryChild.expirationTime; const childChildExpirationTime = primaryChild.childExpirationTime; if ( childUpdateExpirationTime >= renderExpirationTime || childChildExpirationTime >= renderExpirationTime ) { // Found a child with an update with sufficient priority. // Use the normal path to render the primary children again. return updateSuspenseComponent( current, workInProgress, renderExpirationTime, ); } primaryChild = primaryChild.sibling; }
pushSuspenseContext( workInProgress, setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); // The primary children do not have pending work with sufficient // priority. Bailout. const child = bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); if (child !== null) { // The fallback children have pending work. Skip over the // primary children and work on the fallback. return child.sibling; } else { returnnull; } } } else { pushSuspenseContext( workInProgress, setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); } break; } case SuspenseListComponent: { const didSuspendBefore = (current.effectTag & DidCapture) !== NoEffect;
if (didSuspendBefore) { if (hasChildWork) { // If something was in fallback state last time, and we have all the // same children then we're still in progressive loading state. // Something might get unblocked by state updates or retries in the // tree which will affect the tail. So we need to use the normal // path to compute the correct tail. return updateSuspenseListComponent( current, workInProgress, renderExpirationTime, ); } // If none of the children had any work, that means that none of // them got retried so they'll still be blocked in the same way // as before. We can fast bail out. workInProgress.effectTag |= DidCapture; }
// If nothing suspended before and we're rendering the same children, // then the tail doesn't matter. Anything new that suspends will work // in the "together" mode, so we can continue from the state we had. const renderState = workInProgress.memoizedState; if (renderState !== null) { // Reset to the "together" mode in case we've started a different // update in the past but didn't complete it. renderState.rendering = null; renderState.tail = null; renderState.lastEffect = null; } pushSuspenseContext(workInProgress, suspenseStackCursor.current);
if (hasChildWork) { break; } else { // If none of the children had any work, that means that none of // them got retried so they'll still be blocked in the same way // as before. We can fast bail out. returnnull; } } } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } else { // An update was scheduled on this fiber, but there are no new props // nor legacy context. Set this to false. If an update queue or context // consumer produces a changed value, it will set this to true. Otherwise, // the component will assume the children have not changed and bail out. didReceiveUpdate = false; } } else { didReceiveUpdate = false; // 首次渲染直接到这里 }
// Before entering the begin phase, clear pending update priority. // TODO: This assumes that we're about to evaluate the component and process // the update queue. However, there's an exception: SimpleMemoComponent // sometimes bails out later in the begin phase. This indicates that we should // move this assignment out of the common path and into each branch. workInProgress.expirationTime = NoWork;
switch (workInProgress.tag) { case IndeterminateComponent: { return mountIndeterminateComponent( current, workInProgress, workInProgress.type, renderExpirationTime, ); } case LazyComponent: { const elementType = workInProgress.elementType; return mountLazyComponent( current, workInProgress, elementType, updateExpirationTime, renderExpirationTime, ); } case FunctionComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); } case ClassComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateClassComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); } case HostRoot: // 首次渲染第一次进入这个分支 return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: return updateHostComponent(current, workInProgress, renderExpirationTime); case HostText: return updateHostText(current, workInProgress); case SuspenseComponent: return updateSuspenseComponent( current, workInProgress, renderExpirationTime, ); case HostPortal: return updatePortalComponent( current, workInProgress, renderExpirationTime, ); case ForwardRef: { const type = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === type ? unresolvedProps : resolveDefaultProps(type, unresolvedProps); return updateForwardRef( current, workInProgress, type, resolvedProps, renderExpirationTime, ); } case Fragment: return updateFragment(current, workInProgress, renderExpirationTime); case Mode: return updateMode(current, workInProgress, renderExpirationTime); case Profiler: return updateProfiler(current, workInProgress, renderExpirationTime); case ContextProvider: return updateContextProvider( current, workInProgress, renderExpirationTime, ); case ContextConsumer: return updateContextConsumer( current, workInProgress, renderExpirationTime, ); case MemoComponent: { const type = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; // Resolve outer props first, then resolve inner props. let resolvedProps = resolveDefaultProps(type, unresolvedProps); resolvedProps = resolveDefaultProps(type.type, resolvedProps); return updateMemoComponent( current, workInProgress, type, resolvedProps, updateExpirationTime, renderExpirationTime, ); } case SimpleMemoComponent: { return updateSimpleMemoComponent( current, workInProgress, workInProgress.type, workInProgress.pendingProps, updateExpirationTime, renderExpirationTime, ); } case IncompleteClassComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return mountIncompleteClassComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); } case SuspenseListComponent: { return updateSuspenseListComponent( current, workInProgress, renderExpirationTime, ); } case FundamentalComponent: { if (enableFundamentalAPI) { return updateFundamentalComponent( current, workInProgress, renderExpirationTime, ); } break; } case ScopeComponent: { if (enableScopeAPI) { return updateScopeComponent( current, workInProgress, renderExpirationTime, ); } break; } case Block: { if (enableBlocksAPI) { const block = workInProgress.type; const props = workInProgress.pendingProps; return updateBlock( current, workInProgress, block, props, renderExpirationTime, ); } break; } } invariant( false, 'Unknown unit of work tag (%s). This error is likely caused by a bug in ' + 'React. Please file an issue.', workInProgress.tag, ); }
functionpushHostContainer(fiber: Fiber, nextRootInstance: Container) { // Push current root instance onto the stack; // This allows us to reset root when portals are popped. // index = 2 // valueStack = [{}, false, {}] // rootInstanceStackCursor = { current: container } push(rootInstanceStackCursor, nextRootInstance, fiber); // Track the context and the Fiber that provided it. // This enables us to pop only Fibers that provide unique contexts. // index = 3 // valueStack = [{}, false, {}, {}] // contextFiberStackCursor = { current: workInProgress } push(contextFiberStackCursor, fiber, fiber);
// Finally, we need to push the host context to the stack. // However, we can't just call getRootHostContext() and push it because // we'd have a different number of entries on the stack depending on // whether getRootHostContext() throws somewhere in renderer code or not. // So we push an empty value first. This lets us safely unwind on errors.
// index = 4 // valueStack = [{}, false, {}, {}, {}] // contextStackCursor = { current: {} } push(contextStackCursor, NO_CONTEXT, fiber); const nextRootContext = getRootHostContext(nextRootInstance); // 计算得到:http://www.w3.org/1999/xhtml // Now that we know this function doesn't throw, replace it. // index = 3 // valueStack = [{}, false, {}, {}, null] // contextStackCursor = { current: {} } pop(contextStackCursor, fiber); // index = 4 // valueStack = [{}, false, {}, {}, {} ] // contextStackCursor = { current: "http://www.w3.org/1999/xhtml" } push(contextStackCursor, nextRootContext, fiber); }
// Check if the children have any pending work. const childExpirationTime = workInProgress.childExpirationTime; if (childExpirationTime < renderExpirationTime) { // The children don't have any work either. We can skip them. // TODO: Once we add back resuming, we should check if the children are // a work-in-progress set. If so, we need to transfer their effects. returnnull; } else { // This fiber doesn't have work, but its subtree does. Clone the child // fibers and continue. cloneChildFibers(current, workInProgress); return workInProgress.child; } }
functionupdateHostRoot(current, workInProgress, renderExpirationTime) { pushHostRootContext(workInProgress); // 见上 const updateQueue = workInProgress.updateQueue; const nextProps = workInProgress.pendingProps; const prevState = workInProgress.memoizedState; const prevChildren = prevState !== null ? prevState.element : null; // null cloneUpdateQueue(current, workInProgress); processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime); const nextState = workInProgress.memoizedState; // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element; if (nextChildren === prevChildren) { // If the state is the same as before, that's a bailout because we had // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } const root: FiberRoot = workInProgress.stateNode; if (root.hydrate && enterHydrationState(workInProgress)) { // If we don't have any current children this might be the first pass. // We always try to hydrate. If this isn't a hydration pass there won't // be any children to hydrate which is effectively the same thing as // not hydrating.
if (supportsHydration) { const mutableSourceEagerHydrationData = root.mutableSourceEagerHydrationData; if (mutableSourceEagerHydrationData != null) { for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { const mutableSource = ((mutableSourceEagerHydrationData[ i ]: any): MutableSource<any>); const version = mutableSourceEagerHydrationData[i + 1]; setWorkInProgressVersion(mutableSource, version); } } }
let node = child; while (node) { // Mark each child as hydrating. This is a fast path to know whether this // tree is part of a hydrating tree. This is used to determine if a child // node has fully mounted yet, and for scheduling event replaying. // Conceptually this is similar to Placement in that a new subtree is // inserted into the React tree here. It just happens to not need DOM // mutations because it already exists. node.effectTag = (node.effectTag & ~Placement) | Hydrating; node = node.sibling; } } else { // Otherwise reset hydration state in case we aborted and resumed another // root. reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); resetHydrationState(); } return workInProgress.child; }
// Global state that is reset at the beginning of calling `processUpdateQueue`. // It should only be read right after calling `processUpdateQueue`, via // `checkHasForceUpdateAfterProcessing`. let hasForceUpdate = false;
// The work left over by components that were visited during this render. Only // includes unprocessed updates, not work in bailed out children. let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
exportfunctionprocessUpdateQueue<State>( workInProgress: Fiber, props: any, instance: any, renderExpirationTime: ExpirationTime, ): void{ // This is always non-null on a ClassComponent or HostRoot const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
hasForceUpdate = false;
let firstBaseUpdate = queue.firstBaseUpdate; // null let lastBaseUpdate = queue.lastBaseUpdate; // null
// Check if there are pending updates. If so, transfer them to the base queue. let pendingQueue = queue.shared.pending; if (pendingQueue !== null) { queue.shared.pending = null;
// The pending queue is circular. Disconnect the pointer between first // and last so that it's non-circular. const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next; // lastPendingUpdate === firstPendingUpdate true lastPendingUpdate.next = null; // Append pending updates to base queue if (lastBaseUpdate === null) { firstBaseUpdate = firstPendingUpdate; } else { lastBaseUpdate.next = firstPendingUpdate; } lastBaseUpdate = lastPendingUpdate;
// If there's a current queue, and it's different from the base queue, then // we need to transfer the updates to that queue, too. Because the base // queue is a singly-linked list with no cycles, we can append to both // lists and take advantage of structural sharing. // TODO: Pass `current` as argument const current = workInProgress.alternate; if (current !== null) { // This is always non-null on a ClassComponent or HostRoot const currentQueue: UpdateQueue<State> = (current.updateQueue: any); // currentQueue = { // baseState: null, // effects: null, // firstBaseUpdate: null, // lastBaseUpdate: null, // shared: { // pending: null, // }, // } const currentLastBaseUpdate = currentQueue.lastBaseUpdate; if (currentLastBaseUpdate !== lastBaseUpdate) { if (currentLastBaseUpdate === null) { currentQueue.firstBaseUpdate = firstPendingUpdate; } else { currentLastBaseUpdate.next = firstPendingUpdate; } currentQueue.lastBaseUpdate = lastPendingUpdate; } } }
// These values may change as we process the queue. if (firstBaseUpdate !== null) { // Iterate through the list of updates to compute the result. let newState = queue.baseState; let newExpirationTime = NoWork;
let newBaseState = null; let newFirstBaseUpdate = null; let newLastBaseUpdate = null;
let update = firstBaseUpdate; do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. const clone: Update<State> = { expirationTime: update.expirationTime, suspenseConfig: update.suspenseConfig,
next: null, }; if (newLastBaseUpdate === null) { newFirstBaseUpdate = newLastBaseUpdate = clone; newBaseState = newState; } else { newLastBaseUpdate = newLastBaseUpdate.next = clone; } // Update the remaining priority in the queue. if (updateExpirationTime > newExpirationTime) { newExpirationTime = updateExpirationTime; } } else { // This update does have sufficient priority.
if (newLastBaseUpdate !== null) { const clone: Update<State> = { expirationTime: Sync, // This update is going to be committed so we never want uncommit it. suspenseConfig: update.suspenseConfig,
// Mark the event time of this update as relevant to this render pass. // TODO: This should ideally use the true event time of this update rather than // its priority which is a derived and not reverseable value. // TODO: We should skip this update if it was already committed but currently // we have no way of detecting the difference between a committed and suspended // update here. // 首次渲染第一次什么都没干 markRenderEventTimeAndConfig( updateExpirationTime, update.suspenseConfig, );
// Process this update. newState = getStateFromUpdate( workInProgress, queue, update, newState, props, instance, ); const callback = update.callback; if (callback !== null) { workInProgress.effectTag |= Callback; const effects = queue.effects; if (effects === null) { queue.effects = [update]; } else { effects.push(update); } } } update = update.next; if (update === null) { pendingQueue = queue.shared.pending; if (pendingQueue === null) { break; } else { // An update was scheduled from inside a reducer. Add the new // pending updates to the end of the list and keep processing. const lastPendingUpdate = pendingQueue; // Intentionally unsound. Pending updates form a circular list, but we // unravel them when transferring them to the base queue. const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>); lastPendingUpdate.next = null; update = firstPendingUpdate; queue.lastBaseUpdate = lastPendingUpdate; queue.shared.pending = null; } } } while (true);
if (newLastBaseUpdate === null) { newBaseState = newState; }
// Set the remaining expiration time to be whatever is remaining in the queue. // This should be fine because the only two other things that contribute to // expiration time are props and context. We're already in the middle of the // begin phase by the time we start processing the queue, so we've already // dealt with the props. Context in components that specify // shouldComponentUpdate is tricky; but we'll have to account for // that regardless. markUnprocessedUpdateTime(newExpirationTime); workInProgress.expirationTime = newExpirationTime; workInProgress.memoizedState = newState; } }
// Most recent event time among processed updates during this render. // This is conceptually a time stamp but expressed in terms of an ExpirationTime // because we deal mostly with expiration times in the hot path, so this avoids // the conversion happening in the hot path. let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync; // Idle is slightly higher priority than Never. It must completely finish in // order to be consistent. exportconst Idle = 2;
functionmarkRenderEventTimeAndConfig( expirationTime: ExpirationTime, //1073741823 suspenseConfig: null | SuspenseConfig, // null ): void{ if ( expirationTime < workInProgressRootLatestProcessedExpirationTime && expirationTime > Idle ) { workInProgressRootLatestProcessedExpirationTime = expirationTime; } if (suspenseConfig !== null) { if ( expirationTime < workInProgressRootLatestSuspenseTimeout && expirationTime > Idle ) { workInProgressRootLatestSuspenseTimeout = expirationTime; // Most of the time we only have one config and getting wrong is not bad. workInProgressRootCanSuspendUsingConfig = suspenseConfig; } } }
exportfunctionreconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderExpirationTime, ); } else { // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so // let's throw it out.
functionplaceSingleChild(newFiber: Fiber): Fiber{ // This is simpler for the single child case. We only need to do a // placement for inserting new children. if (shouldTrackSideEffects && newFiber.alternate === null) { newFiber.effectTag = Placement; } return newFiber; }
functionreconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, expirationTime: ExpirationTime, ): Fiber{ const key = element.key; let child = currentFirstChild; while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. if (child.key === key) { switch (child.tag) { case Fragment: { if (element.type === REACT_FRAGMENT_TYPE) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, element.props.children); existing.return = returnFiber; return existing; } break; } case Block: if (enableBlocksAPI) { let type = element.type; if (type.$$typeof === REACT_LAZY_TYPE) { type = resolveLazyType(type); } if (type.$$typeof === REACT_BLOCK_TYPE) { // The new Block might not be initialized yet. We need to initialize // it in case initializing it turns out it would match. if ( ((type: any): BlockComponent<any, any>)._render === (child.type: BlockComponent<any, any>)._render ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, element.props); existing.type = type; existing.return = returnFiber; return existing; } } } // We intentionally fallthrough here if enableBlocksAPI is not on. // eslint-disable-next-lined no-fallthrough default: { if ( child.elementType === element.type || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, element.props); existing.ref = coerceRef(returnFiber, child, element); existing.return = returnFiber; return existing; } break; } } // Didn't match. deleteRemainingChildren(returnFiber, child); break; } else { deleteChild(returnFiber, child); } child = child.sibling; }
// This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. functionreconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, expirationTime: ExpirationTime, ): Fiber | null{ // This function is not recursive. // If the top level item is an array, we treat it as a set of children, // not as a fragment. Nested arrays on the other hand will be treated as // fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays. // This leads to an ambiguity between <>{[...]}</> and <>...</>. // We treat the ambiguous cases above the same. const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; // false if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; }
if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); }
if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) { // If the new child is undefined, and the return fiber is a composite // component, throw an error. If Fiber return types are disabled, // we already threw above. switch (returnFiber.tag) { case ClassComponent: { } // Intentionally fall through to the next case, which handles both // functions and classes // eslint-disable-next-lined no-fallthrough case FunctionComponent: { const Component = returnFiber.type; invariant( false, '%s(...): Nothing was returned from render. This usually means a ' + 'return statement is missing. Or, to render nothing, ' + 'return null.', Component.displayName || Component.name || 'Component', ); } } }
// Remaining cases are all treated as empty. return deleteRemainingChildren(returnFiber, currentFirstChild); }
functioncoerceRef( returnFiber: Fiber, // current: Fiber | null, // null element: ReactElement, ) { const mixedRef = element.ref; // null if ( mixedRef !== null && typeof mixedRef !== 'function' && typeof mixedRef !== 'object' ) { if (element._owner) { const owner: ?Fiber = (element._owner: any); let inst; if (owner) { const ownerFiber = ((owner: any): Fiber); invariant( ownerFiber.tag === ClassComponent, 'Function components cannot have string refs. ' + 'We recommend using useRef() instead. ' + 'Learn more about using refs safely here: ' + 'https://fb.me/react-strict-mode-string-ref', ); inst = ownerFiber.stateNode; } invariant( inst, 'Missing owner for string ref %s. This error is likely caused by a ' + 'bug in React. Please file an issue.', mixedRef, ); const stringRef = '' + mixedRef; // Check if previous string ref matches new string ref if ( current !== null && current.ref !== null && typeof current.ref === 'function' && current.ref._stringRef === stringRef ) { return current.ref; } const ref = function(value) { let refs = inst.refs; if (refs === emptyRefsObject) { // This is a lazy pooled frozen object, so we need to initialize. refs = inst.refs = {}; } if (value === null) { delete refs[stringRef]; } else { refs[stringRef] = value; } }; ref._stringRef = stringRef; return ref; } else { invariant( typeof mixedRef === 'string', 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.', ); invariant( element._owner, 'Element ref was specified as a string (%s) but no owner was set. This could happen for one of' + ' the following reasons:\n' + '1. You may be adding a ref to a function component\n' + "2. You may be adding a ref to a component that was not created inside a component's render method\n" + '3. You have multiple copies of React loaded\n' + 'See https://fb.me/react-refs-must-have-owner for more information.', mixedRef, ); } } return mixedRef; }
functiongetUnmaskedContext( workInProgress: Fiber, Component: Function, didPushOwnContextIfProvider: boolean, ): Object{ if (disableLegacyContext) { return emptyContextObject; } else { if (didPushOwnContextIfProvider && isContextProvider(Component)) { // If the fiber is a context provider itself, when we read its context // we may have already pushed its own child context on the stack. A context // provider should not "see" its own child context. Therefore we read the // previous (parent) context instead for a context provider. return previousContext; } return contextStackCursor.current; } }
functiongetMaskedContext( workInProgress: Fiber, unmaskedContext: Object, ): Object{ if (disableLegacyContext) { return emptyContextObject; } else { const type = workInProgress.type; const contextTypes = type.contextTypes; if (!contextTypes) { return emptyContextObject; }
// Avoid recreating masked context unless unmasked context has changed. // Failing to do this will result in unnecessary calls to componentWillReceiveProps. // This may trigger infinite loops if componentWillReceiveProps calls setState. const instance = workInProgress.stateNode; if ( instance && instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext ) { return instance.__reactInternalMemoizedMaskedChildContext; }
const context = {}; for (const key in contextTypes) { context[key] = unmaskedContext[key]; }
if (__DEV__) { const name = getComponentName(type) || 'Unknown'; checkPropTypes(contextTypes, context, 'context', name); }
// Cache unmasked context so we can avoid recreating masked context unless necessary. // Context is created before the class component is instantiated so check for instance. if (instance) { cacheContext(workInProgress, unmaskedContext, context); }
return context; } }
// Don't change these two values. They're used by React Dev Tools. exportconst NoEffect = /* */0b00000000000000; exportconst PerformedWork = /* */0b00000000000001;
functionmountIndeterminateComponent( _current, workInProgress, Component, renderExpirationTime, ) { if (_current !== null) { // An indeterminate component only mounts if it suspended inside a non- // concurrent tree, in an inconsistent state. We want to treat it like // a new mount, even though an empty version of it already committed. // Disconnect the alternate pointers. _current.alternate = null; workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect workInProgress.effectTag |= Placement; }
prepareToReadContext(workInProgress, renderExpirationTime); let value;
value = renderWithHooks( null, workInProgress, Component, props, context, renderExpirationTime, ); // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork;
if ( // Run these checks in production only if the flag is off. // Eventually we'll delete this branch altogether. !disableModulePatternComponents && typeof value === 'object' && value !== null && typeof value.render === 'function' && value.$$typeof === undefined ) { // Proceed under the assumption that this is a class instance workInProgress.tag = ClassComponent;
// Throw out any hooks that were used. workInProgress.memoizedState = null; workInProgress.updateQueue = null;
// Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. let hasContext = false; if (isLegacyContextProvider(Component)) { hasContext = true; pushLegacyContextProvider(workInProgress); } else { hasContext = false; }
// These are set right before calling the component. let renderExpirationTime: ExpirationTime = NoWork; // The work-in-progress fiber. I've named it differently to distinguish it from // the work-in-progress hook. let currentlyRenderingFiber: Fiber = (null: any);
// Where an update was scheduled only during the current render pass. This // gets reset after each attempt. // TODO: Maybe there's some way to consolidate this with // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`. let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Hooks are stored as a linked list on the fiber's memoizedState field. The // current hook list is the list that belongs to the current fiber. The // work-in-progress hook list is a new list that will be added to the // work-in-progress fiber. let currentHook: Hook | null = null; let workInProgressHook: Hook | null = null;
// The following should have already been reset // currentHook = null; // workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. // This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. // Non-stateful hooks (e.g. context) don't get added to memoizedState, // so memoizedState would be null during updates and mounts.
// ReactCurrentDispatcher用来Keeps track of the current dispatcher. ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
// Check if there was a render phase update if (didScheduleRenderPhaseUpdateDuringThisPass) { // Keep rendering in a loop for as long as render phase updates continue to // be scheduled. Use a counter to prevent infinite loops. let numberOfReRenders: number = 0; do { didScheduleRenderPhaseUpdateDuringThisPass = false; invariant( numberOfReRenders < RE_RENDER_LIMIT, 'Too many re-renders. React limits the number of renders to prevent ' + 'an infinite loop.', );
numberOfReRenders += 1;
// Start over from the beginning of the list currentHook = null; workInProgressHook = null;
children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); }
// We can assume the previous dispatcher is always this one, since we set it // at the beginning of the render phase and there's no re-entrancy. ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// This check uses currentHook so that it works the same in DEV and prod bundles. // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
// Don't push this Fiber's context unless it's unique. if (context === nextContext) { return; }
// Track the context and the Fiber that provided it. // This enables us to pop only Fibers that provide unique contexts. push(contextFiberStackCursor, fiber, fiber); push(contextStackCursor, nextContext, fiber); } functionupdateHostComponent(current, workInProgress, renderExpirationTime) { pushHostContext(workInProgress);
if (current === null) { tryToClaimNextHydratableInstance(workInProgress); }
const type = workInProgress.type; const nextProps = workInProgress.pendingProps; const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children; const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) { // We special case a direct text child of a host node. This is a common // case. We won't handle it as a reified child. We will instead handle // this in the host environment that also has access to this prop. That // avoids allocating another HostText fiber and traversing it. nextChildren = null; } elseif (prevProps !== null && shouldSetTextContent(type, prevProps)) { // If we're switching from a direct text child to a normal child, or to // empty, we need to schedule the text content to be reset. workInProgress.effectTag |= ContentReset; }
markRef(current, workInProgress);
// Check the host config to see if the children are offscreen/hidden. if ( workInProgress.mode & ConcurrentMode && renderExpirationTime !== Never && shouldDeprioritizeSubtree(type, nextProps) ) { if (enableSchedulerTracing) { markSpawnedWork(Never); } // Schedule this fiber to re-render at offscreen priority. Then bailout. workInProgress.expirationTime = workInProgress.childExpirationTime = Never; // We should never render the children of a dehydrated boundary until we // upgrade it. We return null instead of bailoutOnAlreadyFinishedWork. returnnull; }
functionreconcileChildrenArray( returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, expirationTime: ExpirationTime, ): Fiber | null{ // This algorithm can't optimize by searching from both ends since we // don't have backpointers on fibers. I'm trying to see how far we can get // with that model. If it ends up not being worth the tradeoffs, we can // add it later.
// Even with a two ended optimization, we'd want to optimize for the case // where there are few changes and brute force the comparison instead of // going for the Map. It'd like to explore hitting that path first in // forward-only mode and only go for the Map once we notice that we need // lots of look ahead. This doesn't handle reversal as well as two ended // search but that's unusual. Besides, for the two ended optimization to // work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case // (adding everything to a Map) in for every insert/move.
// If you change this code, also update reconcileChildrenIterator() which // uses the same algorithm.
let resultingFirstChild: Fiber | null = null; let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } const newFiber = updateSlot( returnFiber, oldFiber, newChildren[newIdx], expirationTime, ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need // a better way to communicate whether this was a miss or null, // boolean, undefined, etc. if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. deleteChild(returnFiber, oldFiber); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { // TODO: Defer siblings if we're not at the right index for this slot. // I.e. if we had null values before, then we want to defer this // for each null value. However, we also don't want to call updateSlot // with the previous one. previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; }
if (newIdx === newChildren.length) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; }
if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild( returnFiber, newChildren[newIdx], expirationTime, ); if (newFiber === null) { continue; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; }
// Add all children to a key map for quick lookups. const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves. for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], expirationTime, ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // The new fiber is a work in progress, but if there exists a // current, that means that we reused the fiber. We need to delete // it from the child list so that we don't add it to the deletion // list. existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key, ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 通过下面两个分支代码创建数组组件元素的兄弟节点关系 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } }
if (shouldTrackSideEffects) { // Any existing children that weren't consumed above were deleted. We need // to add them to the deletion list. existingChildren.forEach(child => deleteChild(returnFiber, child)); }
functioncreateChild( returnFiber: Fiber, newChild: any, expirationTime: ExpirationTime, ): Fiber | null{ if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes don't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a text // node. const created = createFiberFromText( '' + newChild, returnFiber.mode, expirationTime, ); created.return = returnFiber; return created; }
// TODO: For the shouldClone case, this could be micro-optimized a bit by // assuming that after the first child we've already added everything. let childToDelete = currentFirstChild; while (childToDelete !== null) { deleteChild(returnFiber, childToDelete); childToDelete = childToDelete.sibling; } returnnull; }
functioncompleteUnitOfWork(unitOfWork: Fiber): void{ // Attempt to complete the current unit of work, then move to the next // sibling. If there are no more siblings, return to the parent fiber. let completedWork = unitOfWork; do { // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = completedWork.alternate; const returnFiber = completedWork.return;
// Check if the work completed or if something threw. if ((completedWork.effectTag & Incomplete) === NoEffect) { setCurrentDebugFiberInDEV(completedWork); let next; if ( !enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode ) { next = completeWork(current, completedWork, renderExpirationTime); } else { startProfilerTimer(completedWork); next = completeWork(current, completedWork, renderExpirationTime); // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); } resetCurrentDebugFiberInDEV();
if (next !== null) { // Completing this fiber spawned new work. Work on that next. workInProgress = next; return; }
resetChildExpirationTime(completedWork);
if ( returnFiber !== null && // Do not append effects to parents if a sibling failed to complete (returnFiber.effectTag & Incomplete) === NoEffect ) { // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the // side-effect order. if (returnFiber.firstEffect === null) { returnFiber.firstEffect = completedWork.firstEffect; } if (completedWork.lastEffect !== null) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork.firstEffect; } returnFiber.lastEffect = completedWork.lastEffect; }
// If this fiber had side-effects, we append it AFTER the children's // side-effects. We can perform certain side-effects earlier if needed, // by doing multiple passes over the effect list. We don't want to // schedule our own side-effect on our own list because if end up // reusing children we'll schedule this effect onto itself since we're // at the end. const effectTag = completedWork.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect // list. PerformedWork effect is read by React DevTools but shouldn't be // committed. if (effectTag > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork; } else { returnFiber.firstEffect = completedWork; } returnFiber.lastEffect = completedWork; } } } else { // This fiber did not complete because something threw. Pop values off // the stack without entering the complete phase. If this is a boundary, // capture values if possible. const next = unwindWork(completedWork, renderExpirationTime);
// Because this fiber did not complete, don't reset its expiration time.
if (next !== null) { // If completing this work spawned new work, do that next. We'll come // back here again. // Since we're restarting, remove anything that is not a host effect // from the effect tag. next.effectTag &= HostEffectMask; workInProgress = next; return; }
if ( enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode ) { // Record the render duration for the fiber that errored. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
// Include the time spent working on failed children before continuing. let actualDuration = completedWork.actualDuration; let child = completedWork.child; while (child !== null) { actualDuration += child.actualDuration; child = child.sibling; } completedWork.actualDuration = actualDuration; }
if (returnFiber !== null) { // Mark the parent fiber as incomplete and clear its effect list. returnFiber.firstEffect = returnFiber.lastEffect = null; returnFiber.effectTag |= Incomplete; } }
const siblingFiber = completedWork.sibling; if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. workInProgress = siblingFiber; return; } // Otherwise, return to the parent completedWork = returnFiber; // Update the next thing we're working on in case something throws. workInProgress = completedWork; } while (completedWork !== null);
// We've reached the root. if (workInProgressRootExitStatus === RootIncomplete) { workInProgressRootExitStatus = RootCompleted; } }
switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case FunctionComponent: case ForwardRef: case Fragment: case Mode: case Profiler: case ContextConsumer: case MemoComponent: returnnull; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } returnnull; } case HostRoot: { popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); resetMutableSourceWorkInProgressVersions(); const fiberRoot = (workInProgress.stateNode: FiberRoot); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; } if (current === null || current.child === null) { // If we hydrated, pop so that we can delete any remaining children // that weren't hydrated. const wasHydrated = popHydrationState(workInProgress); if (wasHydrated) { // If we hydrated, then we'll need to schedule an update for // the commit side-effects on the root. markUpdate(workInProgress); } elseif (!fiberRoot.hydrate) { // Schedule an effect to clear this container at the start of the next commit. // This handles the case of React rendering into a container with previous children. // It's also safe to do for updates too, because current.child would only be null // if the previous render was null (so the the container would already be empty). workInProgress.effectTag |= Snapshot; } } updateHostContainer(workInProgress); returnnull; } case HostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, );
if (enableDeprecatedFlareAPI) { const prevListeners = current.memoizedProps.DEPRECATED_flareListeners; const nextListeners = newProps.DEPRECATED_flareListeners; if (prevListeners !== nextListeners) { markUpdate(workInProgress); } }
if (current.ref !== workInProgress.ref) { markRef(workInProgress); } } else { if (!newProps) { invariant( workInProgress.stateNode !== null, 'We must have new props for new mounts. This error is likely ' + 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. returnnull; }
const currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context // "stack" as the parent. Then append children as we go in beginWork // or completeWork depending on whether we want to add them top->down or // bottom->up. Top->down is faster in IE11. const wasHydrated = popHydrationState(workInProgress); if (wasHydrated) { // TODO: Move this and createInstance step into the beginPhase // to consolidate. if ( prepareToHydrateHostInstance( workInProgress, rootContainerInstance, currentHostContext, ) ) { // If changes to the hydrated node need to be applied at the // commit-phase we mark this as such. markUpdate(workInProgress); } if (enableDeprecatedFlareAPI) { const listeners = newProps.DEPRECATED_flareListeners; if (listeners != null) { updateDeprecatedEventListeners( listeners, workInProgress, rootContainerInstance, ); } } } else { const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, );
// This needs to be set before we mount Flare event listeners workInProgress.stateNode = instance;
if (enableDeprecatedFlareAPI) { const listeners = newProps.DEPRECATED_flareListeners; if (listeners != null) { updateDeprecatedEventListeners( listeners, workInProgress, rootContainerInstance, ); } }
// Certain renderers require commit-time effects for initial mount. // (eg DOM renderer supports auto-focus for certain elements). // Make sure such renderers get scheduled for later work. if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); } }
if (workInProgress.ref !== null) { // If there is a ref on a host node we need to schedule a callback markRef(workInProgress); } } returnnull; } case HostText: { const newText = newProps; if (current && workInProgress.stateNode != null) { const oldText = current.memoizedProps; // If we have an alternate, that means this is an update and we need // to schedule a side-effect to do the updates. updateHostText(current, workInProgress, oldText, newText); } else { if (typeof newText !== 'string') { invariant( workInProgress.stateNode !== null, 'We must have new props for new mounts. This error is likely ' + 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. } const rootContainerInstance = getRootHostContainer(); const currentHostContext = getHostContext(); const wasHydrated = popHydrationState(workInProgress); if (wasHydrated) { if (prepareToHydrateHostTextInstance(workInProgress)) { markUpdate(workInProgress); } } else { workInProgress.stateNode = createTextInstance( newText, rootContainerInstance, currentHostContext, workInProgress, ); } } returnnull; } case SuspenseComponent: { popSuspenseContext(workInProgress); const nextState: null | SuspenseState = workInProgress.memoizedState;
if (enableSuspenseServerRenderer) { if (nextState !== null && nextState.dehydrated !== null) { if (current === null) { const wasHydrated = popHydrationState(workInProgress); invariant( wasHydrated, 'A dehydrated suspense component was completed without a hydrated node. ' + 'This is probably a bug in React.', ); prepareToHydrateHostSuspenseInstance(workInProgress); if (enableSchedulerTracing) { markSpawnedWork(Never); } returnnull; } else { // We should never have been in a hydration state if we didn't have a current. // However, in some of those paths, we might have reentered a hydration state // and then we might be inside a hydration state. In that case, we'll need to exit out of it. resetHydrationState(); if ((workInProgress.effectTag & DidCapture) === NoEffect) { // This boundary did not suspend so it's now hydrated and unsuspended. workInProgress.memoizedState = null; } // If nothing suspended, we need to schedule an effect to mark this boundary // as having hydrated so events know that they're free to be invoked. // It's also a signal to replay events and the suspense callback. // If something suspended, schedule an effect to attach retry listeners. // So we might as well always mark this. workInProgress.effectTag |= Update; returnnull; } } }
if ((workInProgress.effectTag & DidCapture) !== NoEffect) { // Something suspended. Re-render with the fallback children. workInProgress.expirationTime = renderExpirationTime; if ( enableProfilerTimer && (workInProgress.mode & ProfileMode) !== NoMode ) { transferActualDuration(workInProgress); } // Do not reset the effect list. return workInProgress; }
const nextDidTimeout = nextState !== null; let prevDidTimeout = false; if (current === null) { if (workInProgress.memoizedProps.fallback !== undefined) { popHydrationState(workInProgress); } } else { const prevState: null | SuspenseState = current.memoizedState; prevDidTimeout = prevState !== null; if (!nextDidTimeout && prevState !== null) { // We just switched from the fallback to the normal children. // Delete the fallback. // TODO: Would it be better to store the fallback fragment on // the stateNode during the begin phase? const currentFallbackChild: Fiber | null = (current.child: any) .sibling; if (currentFallbackChild !== null) { // Deletions go at the beginning of the return fiber's effect list const first = workInProgress.firstEffect; if (first !== null) { workInProgress.firstEffect = currentFallbackChild; currentFallbackChild.nextEffect = first; } else { workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild; currentFallbackChild.nextEffect = null; } currentFallbackChild.effectTag = Deletion; } } }
if (nextDidTimeout && !prevDidTimeout) { // If this subtreee is running in blocking mode we can suspend, // otherwise we won't suspend. // TODO: This will still suspend a synchronous tree if anything // in the concurrent tree already suspended during this render. // This is a known bug. if ((workInProgress.mode & BlockingMode) !== NoMode) { // TODO: Move this back to throwException because this is too late // if this is a large tree which is common for initial loads. We // don't know if we should restart a render or not until we get // this marker, and this is too late. // If this render already had a ping or lower pri updates, // and this is the first time we know we're going to suspend we // should be able to immediately restart from within throwException. const hasInvisibleChildContext = current === null && workInProgress.memoizedProps.unstable_avoidThisFallback !== true; if ( hasInvisibleChildContext || hasSuspenseContext( suspenseStackCursor.current, (InvisibleParentSuspenseContext: SuspenseContext), ) ) { // If this was in an invisible tree or a new render, then showing // this boundary is ok. renderDidSuspend(); } else { // Otherwise, we're going to have to hide content so we should // suspend for longer if possible. renderDidSuspendDelayIfPossible(); } } }
if (supportsPersistence) { // TODO: Only schedule updates if not prevDidTimeout. if (nextDidTimeout) { // If this boundary just timed out, schedule an effect to attach a // retry listener to the promise. This flag is also used to hide the // primary children. workInProgress.effectTag |= Update; } } if (supportsMutation) { // TODO: Only schedule updates if these values are non equal, i.e. it changed. if (nextDidTimeout || prevDidTimeout) { // If this boundary just timed out, schedule an effect to attach a // retry listener to the promise. This flag is also used to hide the // primary children. In mutation mode, we also need the flag to // *unhide* children that were previously hidden, so check if this // is currently timed out, too. workInProgress.effectTag |= Update; } } if ( enableSuspenseCallback && workInProgress.updateQueue !== null && workInProgress.memoizedProps.suspenseCallback != null ) { // Always notify the callback workInProgress.effectTag |= Update; } returnnull; } case HostPortal: popHostContainer(workInProgress); updateHostContainer(workInProgress); if (current === null) { preparePortalMount(workInProgress.stateNode.containerInfo); } returnnull; case ContextProvider: // Pop provider fiber popProvider(workInProgress); returnnull; case IncompleteClassComponent: { // Same as class component case. I put it down here so that the tags are // sequential to ensure this switch is compiled to a jump table. const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } returnnull; } case SuspenseListComponent: { popSuspenseContext(workInProgress);
if (renderState === null) { // We're running in the default, "independent" mode. // We don't do anything in this mode. returnnull; }
let didSuspendAlready = (workInProgress.effectTag & DidCapture) !== NoEffect;
const renderedTail = renderState.rendering; if (renderedTail === null) { // We just rendered the head. if (!didSuspendAlready) { // This is the first pass. We need to figure out if anything is still // suspended in the rendered set.
// If new content unsuspended, but there's still some content that // didn't. Then we need to do a second pass that forces everything // to keep showing their fallbacks.
// We might be suspended if something in this render pass suspended, or // something in the previous committed pass suspended. Otherwise, // there's no chance so we can skip the expensive call to // findFirstSuspended. const cannotBeSuspended = renderHasNotSuspendedYet() && (current === null || (current.effectTag & DidCapture) === NoEffect); if (!cannotBeSuspended) { let row = workInProgress.child; while (row !== null) { const suspended = findFirstSuspended(row); if (suspended !== null) { didSuspendAlready = true; workInProgress.effectTag |= DidCapture; cutOffTailIfNeeded(renderState, false);
// If this is a newly suspended tree, it might not get committed as // part of the second pass. In that case nothing will subscribe to // its thennables. Instead, we'll transfer its thennables to the // SuspenseList so that it can retry if they resolve. // There might be multiple of these in the list but since we're // going to wait for all of them anyway, it doesn't really matter // which ones gets to ping. In theory we could get clever and keep // track of how many dependencies remain but it gets tricky because // in the meantime, we can add/remove/change items and dependencies. // We might bail out of the loop before finding any but that // doesn't matter since that means that the other boundaries that // we did find already has their listeners attached. const newThennables = suspended.updateQueue; if (newThennables !== null) { workInProgress.updateQueue = newThennables; workInProgress.effectTag |= Update; }
// Rerender the whole list, but this time, we'll force fallbacks // to stay in place. // Reset the effect list before doing the second pass since that's now invalid. if (renderState.lastEffect === null) { workInProgress.firstEffect = null; } workInProgress.lastEffect = renderState.lastEffect; // Reset the child fibers to their original state. resetChildFibers(workInProgress, renderExpirationTime);
// Set up the Suspense Context to force suspense and immediately // rerender the children. pushSuspenseContext( workInProgress, setShallowSuspenseContext( suspenseStackCursor.current, ForceSuspenseFallback, ), );
return workInProgress.child; } row = row.sibling; } } } else { cutOffTailIfNeeded(renderState, false); } // Next we're going to render the tail. } else { // Append the rendered row to the child list. if (!didSuspendAlready) { const suspended = findFirstSuspended(renderedTail); if (suspended !== null) { workInProgress.effectTag |= DidCapture; didSuspendAlready = true;
// Ensure we transfer the update queue to the parent so that it doesn't // get lost if this row ends up dropped during a second pass. const newThennables = suspended.updateQueue; if (newThennables !== null) { workInProgress.updateQueue = newThennables; workInProgress.effectTag |= Update; }
cutOffTailIfNeeded(renderState, true); // This might have been modified. if ( renderState.tail === null && renderState.tailMode === 'hidden' && !renderedTail.alternate && !getIsHydrating() // We don't cut it if we're hydrating. ) { // We need to delete the row we just rendered. // Reset the effect list to what it was before we rendered this // child. The nested children have already appended themselves. const lastEffect = (workInProgress.lastEffect = renderState.lastEffect); // Remove any effects that were appended after this point. if (lastEffect !== null) { lastEffect.nextEffect = null; } // We're done. returnnull; } } elseif ( // The time it took to render last row is greater than time until // the expiration. now() * 2 - renderState.renderingStartTime > renderState.tailExpiration && renderExpirationTime > Never ) { // We have now passed our CPU deadline and we'll just give up further // attempts to render the main content and only render fallbacks. // The assumption is that this is usually faster. workInProgress.effectTag |= DidCapture; didSuspendAlready = true;
cutOffTailIfNeeded(renderState, false);
// Since nothing actually suspended, there will nothing to ping this // to get it started back up to attempt the next item. If we can show // them, then they really have the same priority as this render. // So we'll pick it back up the very next render pass once we've had // an opportunity to yield for paint.
const nextPriority = renderExpirationTime - 1; workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority; if (enableSchedulerTracing) { markSpawnedWork(nextPriority); } } } if (renderState.isBackwards) { // The effect list of the backwards tail will have been added // to the end. This breaks the guarantee that life-cycles fire in // sibling order but that isn't a strong guarantee promised by React. // Especially since these might also just pop in during future commits. // Append to the beginning of the list. renderedTail.sibling = workInProgress.child; workInProgress.child = renderedTail; } else { const previousSibling = renderState.last; if (previousSibling !== null) { previousSibling.sibling = renderedTail; } else { workInProgress.child = renderedTail; } renderState.last = renderedTail; } }
if (renderState.tail !== null) { // We still have tail rows to render. if (renderState.tailExpiration === 0) { // Heuristic for how long we're willing to spend rendering rows // until we just give up and show what we have so far. const TAIL_EXPIRATION_TIMEOUT_MS = 500; renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS; // TODO: This is meant to mimic the train model or JND but this // is a per component value. It should really be since the start // of the total render or last commit. Consider using something like // globalMostRecentFallbackTime. That doesn't account for being // suspended for part of the time or when it's a new render. // It should probably use a global start time value instead. } // Pop a row. const next = renderState.tail; renderState.rendering = next; renderState.tail = next.sibling; renderState.lastEffect = workInProgress.lastEffect; renderState.renderingStartTime = now(); next.sibling = null;
// Restore the context. // TODO: We can probably just avoid popping it instead and only // setting it the first time we go from not suspended to suspended. let suspenseContext = suspenseStackCursor.current; if (didSuspendAlready) { suspenseContext = setShallowSuspenseContext( suspenseContext, ForceSuspenseFallback, ); } else { suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); } pushSuspenseContext(workInProgress, suspenseContext); // Do a pass over the next row. return next; } returnnull; } case FundamentalComponent: { if (enableFundamentalAPI) { const fundamentalImpl = workInProgress.type.impl; let fundamentalInstance: ReactFundamentalComponentInstance< any, any, > | null = workInProgress.stateNode;
if (fundamentalInstance === null) { const getInitialState = fundamentalImpl.getInitialState; let fundamentalState; if (getInitialState !== undefined) { fundamentalState = getInitialState(newProps); } fundamentalInstance = workInProgress.stateNode = createFundamentalStateInstance( workInProgress, newProps, fundamentalImpl, fundamentalState || {}, ); const instance = ((getFundamentalComponentInstance( fundamentalInstance, ): any): Instance); fundamentalInstance.instance = instance; if (fundamentalImpl.reconcileChildren === false) { returnnull; } appendAllChildren(instance, workInProgress, false, false); mountFundamentalComponent(fundamentalInstance); } else { // We fire update in commit phase const prevProps = fundamentalInstance.props; fundamentalInstance.prevProps = prevProps; fundamentalInstance.props = newProps; fundamentalInstance.currentFiber = workInProgress; if (supportsPersistence) { const instance = cloneFundamentalInstance(fundamentalInstance); fundamentalInstance.instance = instance; appendAllChildren(instance, workInProgress, false, false); } const shouldUpdate = shouldUpdateFundamentalComponent( fundamentalInstance, ); if (shouldUpdate) { markUpdate(workInProgress); } } returnnull; } break; } case ScopeComponent: { if (enableScopeAPI) { if (current === null) { const scopeInstance: ReactScopeInstance = createScopeInstance(); workInProgress.stateNode = scopeInstance; if (enableDeprecatedFlareAPI) { const listeners = newProps.DEPRECATED_flareListeners; if (listeners != null) { const rootContainerInstance = getRootHostContainer(); updateDeprecatedEventListeners( listeners, workInProgress, rootContainerInstance, ); } } prepareScopeUpdate(scopeInstance, workInProgress); if (workInProgress.ref !== null) { markRef(workInProgress); markUpdate(workInProgress); } } else { if (enableDeprecatedFlareAPI) { const prevListeners = current.memoizedProps.DEPRECATED_flareListeners; const nextListeners = newProps.DEPRECATED_flareListeners; if ( prevListeners !== nextListeners || workInProgress.ref !== null ) { markUpdate(workInProgress); } } else { if (workInProgress.ref !== null) { markUpdate(workInProgress); } } if (current.ref !== workInProgress.ref) { markRef(workInProgress); } } returnnull; } break; } case Block: if (enableBlocksAPI) { returnnull; } break; } invariant( false, 'Unknown unit of work tag (%s). This error is likely caused by a bug in ' + 'React. Please file an issue.', workInProgress.tag, ); }
appendAllChildren = function( parent: Instance, workInProgress: Fiber, needsVisibilityToggle: boolean, isHidden: boolean, ) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; while (node !== null) { if (node.tag === HostComponent || node.tag === HostText) { appendInitialChild(parent, node.stateNode); } elseif (enableFundamentalAPI && node.tag === FundamentalComponent) { appendInitialChild(parent, node.stateNode.instance); } elseif (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. } elseif (node.child !== null) { node.child.return = node; node = node.child; continue; } if (node === workInProgress) { return; } while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; } };