UNPKG

react-dom

Version:

React package for working with the DOM.

1,105 lines (977 loc) 40.7 kB
/** * Copyright 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * */ 'use strict'; var _require = require('./ReactFiberContext'), popContextProvider = _require.popContextProvider; var _require2 = require('./ReactFiberStack'), reset = _require2.reset; var ReactFiberBeginWork = require('./ReactFiberBeginWork'); var ReactFiberCompleteWork = require('./ReactFiberCompleteWork'); var ReactFiberCommitWork = require('./ReactFiberCommitWork'); var ReactFiberHostContext = require('./ReactFiberHostContext'); var ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); var _require3 = require('./ReactFiber'), cloneFiber = _require3.cloneFiber; var _require4 = require('./ReactPriorityLevel'), NoWork = _require4.NoWork, SynchronousPriority = _require4.SynchronousPriority, TaskPriority = _require4.TaskPriority, AnimationPriority = _require4.AnimationPriority, HighPriority = _require4.HighPriority, LowPriority = _require4.LowPriority, OffscreenPriority = _require4.OffscreenPriority; var _require5 = require('./ReactTypeOfSideEffect'), NoEffect = _require5.NoEffect, Placement = _require5.Placement, Update = _require5.Update, PlacementAndUpdate = _require5.PlacementAndUpdate, Deletion = _require5.Deletion, ContentReset = _require5.ContentReset, Callback = _require5.Callback, Err = _require5.Err, Ref = _require5.Ref; var _require6 = require('./ReactTypeOfWork'), HostRoot = _require6.HostRoot, HostComponent = _require6.HostComponent, HostPortal = _require6.HostPortal, ClassComponent = _require6.ClassComponent; var _require7 = require('./ReactFiberUpdateQueue'), getPendingPriority = _require7.getPendingPriority; var _require8 = require('./ReactFiberContext'), resetContext = _require8.resetContext; if (process.env.NODE_ENV !== 'production') { var ReactFiberInstrumentation = require('./ReactFiberInstrumentation'); var ReactDebugCurrentFiber = require('./ReactDebugCurrentFiber'); } var timeHeuristicForUnitOfWork = 1; module.exports = function (config) { var hostContext = ReactFiberHostContext(config); var popHostContainer = hostContext.popHostContainer, popHostContext = hostContext.popHostContext, resetHostContainer = hostContext.resetHostContainer; var _ReactFiberBeginWork = ReactFiberBeginWork(config, hostContext, scheduleUpdate, getPriorityContext), beginWork = _ReactFiberBeginWork.beginWork, beginFailedWork = _ReactFiberBeginWork.beginFailedWork; var _ReactFiberCompleteWo = ReactFiberCompleteWork(config, hostContext), completeWork = _ReactFiberCompleteWo.completeWork; var _ReactFiberCommitWork = ReactFiberCommitWork(config, hostContext, captureError), commitPlacement = _ReactFiberCommitWork.commitPlacement, commitDeletion = _ReactFiberCommitWork.commitDeletion, commitWork = _ReactFiberCommitWork.commitWork, commitLifeCycles = _ReactFiberCommitWork.commitLifeCycles, commitRef = _ReactFiberCommitWork.commitRef; var hostScheduleAnimationCallback = config.scheduleAnimationCallback, hostScheduleDeferredCallback = config.scheduleDeferredCallback, useSyncScheduling = config.useSyncScheduling, prepareForCommit = config.prepareForCommit, resetAfterCommit = config.resetAfterCommit; // The priority level to use when scheduling an update. // TODO: Should we change this to an array? Might be less confusing. var priorityContext = useSyncScheduling ? SynchronousPriority : LowPriority; // Keep track of this so we can reset the priority context if an error // is thrown during reconciliation. var priorityContextBeforeReconciliation = NoWork; // Keeps track of whether we're currently in a work loop. var isPerformingWork = false; // Keeps track of whether we should should batch sync updates. var isBatchingUpdates = false; // The next work in progress fiber that we're currently working on. var nextUnitOfWork = null; var nextPriorityLevel = NoWork; // The next fiber with an effect that we're currently committing. var nextEffect = null; var pendingCommit = null; // Linked list of roots with scheduled work on them. var nextScheduledRoot = null; var lastScheduledRoot = null; // Keep track of which host environment callbacks are scheduled. var isAnimationCallbackScheduled = false; var isDeferredCallbackScheduled = false; // Keep track of which fibers have captured an error that need to be handled. // Work is removed from this collection after unstable_handleError is called. var capturedErrors = null; // Keep track of which fibers have failed during the current batch of work. // This is a different set than capturedErrors, because it is not reset until // the end of the batch. This is needed to propagate errors correctly if a // subtree fails more than once. var failedBoundaries = null; // Error boundaries that captured an error during the current commit. var commitPhaseBoundaries = null; var firstUncaughtError = null; var fatalError = null; var isCommitting = false; var isUnmounting = false; function scheduleAnimationCallback(callback) { if (!isAnimationCallbackScheduled) { isAnimationCallbackScheduled = true; hostScheduleAnimationCallback(callback); } } function scheduleDeferredCallback(callback) { if (!isDeferredCallbackScheduled) { isDeferredCallbackScheduled = true; hostScheduleDeferredCallback(callback); } } function resetContextStack() { // Reset the stack reset(); // Reset the cursors resetContext(); resetHostContainer(); } // findNextUnitOfWork mutates the current priority context. It is reset after // after the workLoop exits, so never call findNextUnitOfWork from outside // the work loop. function findNextUnitOfWork() { // Clear out roots with no more work on them, or if they have uncaught errors while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) { // Unschedule this root. nextScheduledRoot.isScheduled = false; // Read the next pointer now. // We need to clear it in case this root gets scheduled again later. var next = nextScheduledRoot.nextScheduledRoot; nextScheduledRoot.nextScheduledRoot = null; // Exit if we cleared all the roots and there's no work to do. if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; nextPriorityLevel = NoWork; return null; } // Continue with the next root. // If there's no work on it, it will get unscheduled too. nextScheduledRoot = next; } var root = nextScheduledRoot; var highestPriorityRoot = null; var highestPriorityLevel = NoWork; while (root) { if (root.current.pendingWorkPriority !== NoWork && (highestPriorityLevel === NoWork || highestPriorityLevel > root.current.pendingWorkPriority)) { highestPriorityLevel = root.current.pendingWorkPriority; highestPriorityRoot = root; } // We didn't find anything to do in this root, so let's try the next one. root = root.nextScheduledRoot; } if (highestPriorityRoot) { nextPriorityLevel = highestPriorityLevel; priorityContext = nextPriorityLevel; // Before we start any new work, let's make sure that we have a fresh // stack to work from. // TODO: This call is burried a bit too deep. It would be nice to have // a single point which happens right before any new work and // unfortunately this is it. resetContextStack(); return cloneFiber(highestPriorityRoot.current, highestPriorityLevel); } nextPriorityLevel = NoWork; return null; } function commitAllHostEffects() { while (nextEffect) { if (process.env.NODE_ENV !== 'production') { ReactDebugCurrentFiber.current = nextEffect; } if (nextEffect.effectTag & ContentReset) { config.resetTextContent(nextEffect.stateNode); } // The following switch statement is only concerned about placement, // updates, and deletions. To avoid needing to add a case for every // possible bitmap value, we remove the secondary effects from the // effect tag and switch on that value. var primaryEffectTag = nextEffect.effectTag & ~(Callback | Err | ContentReset | Ref); switch (primaryEffectTag) { case Placement: { commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. // TODO: findDOMNode doesn't rely on this any more but isMounted // does and isMounted is deprecated anyway so we should be able // to kill this. nextEffect.effectTag &= ~Placement; break; } case PlacementAndUpdate: { // Placement commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. nextEffect.effectTag &= ~Placement; // Update var current = nextEffect.alternate; commitWork(current, nextEffect); break; } case Update: { var _current = nextEffect.alternate; commitWork(_current, nextEffect); break; } case Deletion: { isUnmounting = true; commitDeletion(nextEffect); isUnmounting = false; break; } } nextEffect = nextEffect.nextEffect; } if (process.env.NODE_ENV !== 'production') { ReactDebugCurrentFiber.current = null; } } function commitAllLifeCycles() { while (nextEffect) { var current = nextEffect.alternate; // Use Task priority for lifecycle updates if (nextEffect.effectTag & (Update | Callback)) { commitLifeCycles(current, nextEffect); } if (nextEffect.effectTag & Ref) { commitRef(nextEffect); } if (nextEffect.effectTag & Err) { commitErrorHandling(nextEffect); } var next = nextEffect.nextEffect; // Ensure that we clean these up so that we don't accidentally keep them. // I'm not actually sure this matters because we can't reset firstEffect // and lastEffect since they're on every node, not just the effectful // ones. So we have to clean everything as we reuse nodes anyway. nextEffect.nextEffect = null; // Ensure that we reset the effectTag here so that we can rely on effect // tags to reason about the current life-cycle. nextEffect = next; } } function commitAllWork(finishedWork) { // We keep track of this so that captureError can collect any boundaries // that capture an error during the commit phase. The reason these aren't // local to this function is because errors that occur during cWU are // captured elsewhere, to prevent the unmount from being interrupted. isCommitting = true; pendingCommit = null; var root = finishedWork.stateNode; if (root.current === finishedWork) { throw new Error('Cannot commit the same tree as before. This is probably a bug ' + 'related to the return field.'); } root.current = finishedWork; // Updates that occur during the commit phase should have Task priority var previousPriorityContext = priorityContext; priorityContext = TaskPriority; var firstEffect = void 0; if (finishedWork.effectTag !== NoEffect) { // A fiber's effect list consists only of its children, not itself. So if // the root has an effect, we need to add it to the end of the list. The // resulting list is the set that would belong to the root's parent, if // it had one; that is, all the effects in the tree including the root. if (finishedWork.lastEffect) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { // There is no effect on the root. firstEffect = finishedWork.firstEffect; } var commitInfo = prepareForCommit(); // Commit all the side-effects within a tree. We'll do this in two passes. // The first pass performs all the host insertions, updates, deletions and // ref unmounts. nextEffect = firstEffect; while (nextEffect) { try { commitAllHostEffects(finishedWork); } catch (error) { if (!nextEffect) { throw new Error('Should have nextEffect.'); } captureError(nextEffect, error); // Clean-up if (nextEffect) { nextEffect = nextEffect.nextEffect; } } } resetAfterCommit(commitInfo); // In the second pass we'll perform all life-cycles and ref callbacks. // Life-cycles happen as a separate pass so that all placements, updates, // and deletions in the entire tree have already been invoked. // This pass also triggers any renderer-specific initial effects. nextEffect = firstEffect; while (nextEffect) { try { commitAllLifeCycles(finishedWork, nextEffect); } catch (error) { if (!nextEffect) { throw new Error('Should have nextEffect.'); } captureError(nextEffect, error); if (nextEffect) { nextEffect = nextEffect.nextEffect; } } } isCommitting = false; // If we caught any errors during this commit, schedule their boundaries // to update. if (commitPhaseBoundaries) { commitPhaseBoundaries.forEach(scheduleErrorRecovery); commitPhaseBoundaries = null; } priorityContext = previousPriorityContext; } function resetWorkPriority(workInProgress) { var newPriority = NoWork; // Check for pending update priority. This is usually null so it shouldn't // be a perf issue. var queue = workInProgress.updateQueue; if (queue) { newPriority = getPendingPriority(queue); } // progressedChild is going to be the child set with the highest priority. // Either it is the same as child, or it just bailed out because it choose // not to do the work. var child = workInProgress.progressedChild; while (child) { // Ensure that remaining work priority bubbles up. if (child.pendingWorkPriority !== NoWork && (newPriority === NoWork || newPriority > child.pendingWorkPriority)) { newPriority = child.pendingWorkPriority; } child = child.sibling; } workInProgress.pendingWorkPriority = newPriority; } function completeUnitOfWork(workInProgress) { while (true) { // 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. var current = workInProgress.alternate; var next = completeWork(current, workInProgress); // The work is now done. We don't need this anymore. This flags // to the system not to redo any work here. workInProgress.pendingProps = null; var returnFiber = workInProgress['return']; var siblingFiber = workInProgress.sibling; resetWorkPriority(workInProgress); if (next) { // If completing this work spawned new work, do that next. We'll come // back here again. return next; } if (returnFiber) { // 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) { returnFiber.firstEffect = workInProgress.firstEffect; } if (workInProgress.lastEffect) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.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. if (workInProgress.effectTag !== NoEffect) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; } } if (siblingFiber) { // If there is more work to do in this returnFiber, do that next. return siblingFiber; } else if (returnFiber) { // If there's no more work in this returnFiber. Complete the returnFiber. workInProgress = returnFiber; continue; } else { // We've reached the root. Unless we're current performing deferred // work, we should commit the completed work immediately. If we are // performing deferred work, returning null indicates to the caller // that we just completed the root so they can handle that case correctly. if (nextPriorityLevel < HighPriority) { // Otherwise, we should commit immediately. commitAllWork(workInProgress); } else { pendingCommit = workInProgress; } return null; } } } function performUnitOfWork(workInProgress) { // 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. var current = workInProgress.alternate; if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress); } // See if beginning this work spawns more work. var next = beginWork(current, workInProgress, nextPriorityLevel); if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress); } if (!next) { if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress); } // If this doesn't spawn new work, complete the current work. next = completeUnitOfWork(workInProgress); if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress); } } ReactCurrentOwner.current = null; if (process.env.NODE_ENV !== 'production') { ReactDebugCurrentFiber.current = null; } return next; } function performFailedUnitOfWork(workInProgress) { // 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. var current = workInProgress.alternate; if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress); } // See if beginning this work spawns more work. var next = beginFailedWork(current, workInProgress, nextPriorityLevel); if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress); } if (!next) { if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress); } // If this doesn't spawn new work, complete the current work. next = completeUnitOfWork(workInProgress); if (process.env.NODE_ENV !== 'production' && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress); } } ReactCurrentOwner.current = null; if (process.env.NODE_ENV !== 'production') { ReactDebugCurrentFiber.current = null; } return next; } function performDeferredWork(deadline) { // We pass the lowest deferred priority here because it acts as a minimum. // Higher priorities will also be performed. isDeferredCallbackScheduled = false; performWork(OffscreenPriority, deadline); } function performAnimationWork() { isAnimationCallbackScheduled = false; performWork(AnimationPriority); } function clearErrors() { if (!nextUnitOfWork) { nextUnitOfWork = findNextUnitOfWork(); } // Keep performing work until there are no more errors while (capturedErrors && capturedErrors.size && nextUnitOfWork && nextPriorityLevel !== NoWork && nextPriorityLevel <= TaskPriority) { if (hasCapturedError(nextUnitOfWork)) { // Use a forked version of performUnitOfWork nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork); } else { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (!nextUnitOfWork) { // If performUnitOfWork returns null, that means we just comitted // a root. Normally we'd need to clear any errors that were scheduled // during the commit phase. But we're already clearing errors, so // we can continue. nextUnitOfWork = findNextUnitOfWork(); } } } function workLoop(priorityLevel, deadline, deadlineHasExpired) { // Clear any errors. clearErrors(); if (!nextUnitOfWork) { nextUnitOfWork = findNextUnitOfWork(); } // If there's a deadline, and we're not performing Task work, perform work // using this loop that checks the deadline on every iteration. if (deadline && priorityLevel > TaskPriority) { // The deferred work loop will run until there's no time left in // the current frame. while (nextUnitOfWork && !deadlineHasExpired) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // In a deferred work batch, iff nextUnitOfWork returns null, we just // completed a root and a pendingCommit exists. Logically, we could // omit either of the checks in the following condition, but we need // both to satisfy Flow. if (!nextUnitOfWork && pendingCommit) { // If we have time, we should commit the work now. if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { commitAllWork(pendingCommit); nextUnitOfWork = findNextUnitOfWork(); // Clear any errors that were scheduled during the commit phase. clearErrors(); } else { deadlineHasExpired = true; } // Otherwise the root will committed in the next frame. } } else { deadlineHasExpired = true; } } } else { // If there's no deadline, or if we're performing Task work, use this loop // that doesn't check how much time is remaining. It will keep running // until we run out of work at this priority level. while (nextUnitOfWork && nextPriorityLevel !== NoWork && nextPriorityLevel <= priorityLevel) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (!nextUnitOfWork) { nextUnitOfWork = findNextUnitOfWork(); // performUnitOfWork returned null, which means we just comitted a // root. Clear any errors that were scheduled during the commit phase. clearErrors(); } } } return deadlineHasExpired; } function performWork(priorityLevel, deadline) { if (isPerformingWork) { throw new Error('performWork was called recursively.'); } isPerformingWork = true; var isPerformingDeferredWork = Boolean(deadline); var deadlineHasExpired = false; // This outer loop exists so that we can restart the work loop after // catching an error. It also lets us flush Task work at the end of a // deferred batch. while (priorityLevel !== NoWork && !fatalError) { if (priorityLevel >= HighPriority && !deadline) { throw new Error('Cannot perform deferred work without a deadline.'); } // Before starting any work, check to see if there are any pending // commits from the previous frame. if (pendingCommit && !deadlineHasExpired) { commitAllWork(pendingCommit); } // Nothing in performWork should be allowed to throw. All unsafe // operations must happen within workLoop, which is extracted to a // separate function so that it can be optimized by the JS engine. try { priorityContextBeforeReconciliation = priorityContext; priorityContext = nextPriorityLevel; deadlineHasExpired = workLoop(priorityLevel, deadline, deadlineHasExpired); } catch (error) { // We caught an error during either the begin or complete phases. var failedWork = nextUnitOfWork; if (failedWork) { // Reset the priority context to its value before reconcilation. priorityContext = priorityContextBeforeReconciliation; // "Capture" the error by finding the nearest boundary. If there is no // error boundary, the nearest host container acts as one. If // captureError returns null, the error was intentionally ignored. var maybeBoundary = captureError(failedWork, error); if (maybeBoundary) { var boundary = maybeBoundary; // Complete the boundary as if it rendered null. This will unmount // the failed tree. beginFailedWork(boundary.alternate, boundary, priorityLevel); // The next unit of work is now the boundary that captured the error. // Conceptually, we're unwinding the stack. We need to unwind the // context stack, too, from the failed work to the boundary that // captured the error. // TODO: If we set the memoized props in beginWork instead of // completeWork, rather than unwind the stack, we can just restart // from the root. Can't do that until then because without memoized // props, the nodes higher up in the tree will rerender unnecessarily. unwindContexts(failedWork, boundary); nextUnitOfWork = completeUnitOfWork(boundary); } // Continue performing work continue; } else if (!fatalError) { // There is no current unit of work. This is a worst-case scenario // and should only be possible if there's a bug in the renderer, e.g. // inside resetAfterCommit. fatalError = error; } } finally { priorityContext = priorityContextBeforeReconciliation; } // Stop performing work priorityLevel = NoWork; // If have we more work, and we're in a deferred batch, check to see // if the deadline has expired. if (nextPriorityLevel !== NoWork && isPerformingDeferredWork && !deadlineHasExpired) { // We have more time to do work. priorityLevel = nextPriorityLevel; continue; } // There might be work left. Depending on the priority, we should // either perform it now or schedule a callback to perform it later. switch (nextPriorityLevel) { case SynchronousPriority: case TaskPriority: // Perform work immediately by switching the priority level // and continuing the loop. priorityLevel = nextPriorityLevel; break; case AnimationPriority: scheduleAnimationCallback(performAnimationWork); // Even though the next unit of work has animation priority, there // may still be deferred work left over as well. I think this is // only important for unit tests. In a real app, a deferred callback // would be scheduled during the next animation frame. scheduleDeferredCallback(performDeferredWork); break; case HighPriority: case LowPriority: case OffscreenPriority: scheduleDeferredCallback(performDeferredWork); break; } } var errorToThrow = fatalError || firstUncaughtError; // We're done performing work. Time to clean up. isPerformingWork = false; fatalError = null; firstUncaughtError = null; capturedErrors = null; failedBoundaries = null; // It's safe to throw any unhandled errors. if (errorToThrow) { throw errorToThrow; } } // Returns the boundary that captured the error, or null if the error is ignored function captureError(failedWork, error) { // It is no longer valid because we exited the user code. ReactCurrentOwner.current = null; if (process.env.NODE_ENV !== 'production') { ReactDebugCurrentFiber.current = null; } // It is no longer valid because this unit of work failed. nextUnitOfWork = null; // Search for the nearest error boundary. var boundary = null; // Host containers are a special case. If the failed work itself is a host // container, then it acts as its own boundary. In all other cases, we // ignore the work itself and only search through the parents. if (failedWork.tag === HostRoot) { boundary = failedWork; if (isFailedBoundary(failedWork)) { // If this root already failed, there must have been an error when // attempting to unmount it. This is a worst-case scenario and // should only be possible if there's a bug in the renderer. fatalError = error; } } else { var node = failedWork['return']; while (node && !boundary) { if (node.tag === ClassComponent) { var instance = node.stateNode; if (typeof instance.unstable_handleError === 'function') { if (isFailedBoundary(node)) { // This boundary is already in a failed state. The error should // propagate to the next boundary — except in the // following cases: // If we're currently unmounting, that means this error was // thrown while unmounting a failed subtree. We should ignore // the error. if (isUnmounting) { return null; } // If we're in the commit phase, we should check to see if // this boundary already captured an error during this commit. // This case exists because multiple errors can be thrown during // a single commit without interruption. if (commitPhaseBoundaries && (commitPhaseBoundaries.has(node) || node.alternate && commitPhaseBoundaries.has(node.alternate))) { // If so, we should ignore this error. return null; } } else { // Found an error boundary! boundary = node; } } } else if (node.tag === HostRoot) { // Treat the root like a no-op error boundary. boundary = node; } node = node['return']; } } if (boundary) { // Add to the collection of failed boundaries. This lets us know that // subsequent errors in this subtree should propagate to the next boundary. if (!failedBoundaries) { failedBoundaries = new Set(); } failedBoundaries.add(boundary); // Add to the collection of captured errors. This is stored as a global // map of errors keyed by the boundaries that capture them. We mostly // use this Map as a Set; it's a Map only to avoid adding a field to Fiber // to store the error. if (!capturedErrors) { capturedErrors = new Map(); } capturedErrors.set(boundary, error); // If we're in the commit phase, defer scheduling an update on the // boundary until after the commit is complete if (isCommitting) { if (!commitPhaseBoundaries) { commitPhaseBoundaries = new Set(); } commitPhaseBoundaries.add(boundary); } else { // Otherwise, schedule an update now. scheduleErrorRecovery(boundary); } return boundary; } else if (!firstUncaughtError) { // If no boundary is found, we'll need to throw the error firstUncaughtError = error; } return null; } function hasCapturedError(fiber) { // TODO: capturedErrors should store the boundary instance, to avoid needing // to check the alternate. return Boolean(capturedErrors && (capturedErrors.has(fiber) || fiber.alternate && capturedErrors.has(fiber.alternate))); } function isFailedBoundary(fiber) { // TODO: failedBoundaries should store the boundary instance, to avoid // needing to check the alternate. return Boolean(failedBoundaries && (failedBoundaries.has(fiber) || fiber.alternate && failedBoundaries.has(fiber.alternate))); } function commitErrorHandling(effectfulFiber) { var error = void 0; if (capturedErrors) { error = capturedErrors.get(effectfulFiber); capturedErrors['delete'](effectfulFiber); if (!error) { if (effectfulFiber.alternate) { effectfulFiber = effectfulFiber.alternate; error = capturedErrors.get(effectfulFiber); capturedErrors['delete'](effectfulFiber); } } } if (!error) { throw new Error('No error for given unit of work.'); } switch (effectfulFiber.tag) { case ClassComponent: var instance = effectfulFiber.stateNode; // Allow the boundary to handle the error, usually by scheduling // an update to itself instance.unstable_handleError(error); return; case HostRoot: if (!firstUncaughtError) { // If this is the host container, we treat it as a no-op error // boundary. We'll throw the first uncaught error once it's safe to // do so, at the end of the batch. firstUncaughtError = error; } return; default: throw new Error('Invalid type of work.'); } } function unwindContexts(from, to) { var node = from; while (node && node !== to && node.alternate !== to) { switch (node.tag) { case ClassComponent: popContextProvider(node); break; case HostComponent: popHostContext(node); break; case HostRoot: popHostContainer(node); break; case HostPortal: popHostContainer(node); break; } node = node['return']; } } function scheduleRoot(root, priorityLevel) { if (priorityLevel === NoWork) { return; } if (!root.isScheduled) { root.isScheduled = true; if (lastScheduledRoot) { // Schedule ourselves to the end. lastScheduledRoot.nextScheduledRoot = root; lastScheduledRoot = root; } else { // We're the only work scheduled. nextScheduledRoot = root; lastScheduledRoot = root; } } } function scheduleUpdate(fiber, priorityLevel) { if (priorityLevel <= nextPriorityLevel) { // We must reset the current unit of work pointer so that we restart the // search from the root during the next tick, in case there is now higher // priority work somewhere earlier than before. nextUnitOfWork = null; } var node = fiber; var shouldContinue = true; while (node && shouldContinue) { // Walk the parent path to the root and update each node's priority. Once // we reach a node whose priority matches (and whose alternate's priority // matches) we can exit safely knowing that the rest of the path is correct. shouldContinue = false; if (node.pendingWorkPriority === NoWork || node.pendingWorkPriority > priorityLevel) { // Priority did not match. Update and keep going. shouldContinue = true; node.pendingWorkPriority = priorityLevel; } if (node.alternate) { if (node.alternate.pendingWorkPriority === NoWork || node.alternate.pendingWorkPriority > priorityLevel) { // Priority did not match. Update and keep going. shouldContinue = true; node.alternate.pendingWorkPriority = priorityLevel; } } if (!node['return']) { if (node.tag === HostRoot) { var root = node.stateNode; scheduleRoot(root, priorityLevel); // Depending on the priority level, either perform work now or // schedule a callback to perform work later. switch (priorityLevel) { case SynchronousPriority: performWork(SynchronousPriority); return; case TaskPriority: // TODO: If we're not already performing work, schedule a // deferred callback. return; case AnimationPriority: scheduleAnimationCallback(performAnimationWork); return; case HighPriority: case LowPriority: case OffscreenPriority: scheduleDeferredCallback(performDeferredWork); return; } } else { // TODO: Warn about setting state on an unmounted component. return; } } node = node['return']; } } function getPriorityContext() { // If we're in a batch, or if we're already performing work, downgrade sync // priority to task priority if (priorityContext === SynchronousPriority && (isPerformingWork || isBatchingUpdates)) { return TaskPriority; } return priorityContext; } function scheduleErrorRecovery(fiber) { scheduleUpdate(fiber, TaskPriority); } function performWithPriority(priorityLevel, fn) { var previousPriorityContext = priorityContext; priorityContext = priorityLevel; try { fn(); } finally { priorityContext = previousPriorityContext; } } function batchedUpdates(fn, a) { var previousIsBatchingUpdates = isBatchingUpdates; isBatchingUpdates = true; try { return fn(a); } finally { isBatchingUpdates = previousIsBatchingUpdates; // If we're not already inside a batch, we need to flush any task work // that was created by the user-provided function. if (!isPerformingWork && !isBatchingUpdates) { performWork(TaskPriority); } } } function unbatchedUpdates(fn) { var previousIsBatchingUpdates = isBatchingUpdates; isBatchingUpdates = false; try { return fn(); } finally { isBatchingUpdates = previousIsBatchingUpdates; } } function syncUpdates(fn) { var previousPriorityContext = priorityContext; priorityContext = SynchronousPriority; try { return fn(); } finally { priorityContext = previousPriorityContext; } } function deferredUpdates(fn) { var previousPriorityContext = priorityContext; priorityContext = LowPriority; try { return fn(); } finally { priorityContext = previousPriorityContext; } } return { scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, performWithPriority: performWithPriority, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, syncUpdates: syncUpdates, deferredUpdates: deferredUpdates }; };