From 010c4edea044a2de4bbfdac8fee64498ab4e2328 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 12 Mar 2018 16:07:14 -0700 Subject: [PATCH 1/3] Use module pattern so context stack is isolated per renderer --- .../src/ReactFiberBeginWork.js | 25 +- .../src/ReactFiberClassComponent.js | 17 +- .../src/ReactFiberCompleteWork.js | 16 +- .../react-reconciler/src/ReactFiberContext.js | 514 ++++++++++-------- .../src/ReactFiberHostContext.js | 6 +- .../src/ReactFiberNewContext.js | 114 ++-- .../src/ReactFiberReconciler.js | 40 +- .../src/ReactFiberScheduler.js | 32 +- .../react-reconciler/src/ReactFiberStack.js | 106 ++-- .../src/ReactFiberUnwindWork.js | 33 +- 10 files changed, 510 insertions(+), 393 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 95914814bc1..ca8c94288c5 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -11,6 +11,8 @@ import type {HostConfig} from 'react-reconciler'; import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {HostContext} from './ReactFiberHostContext'; +import type {LegacyContext} from './ReactFiberContext'; +import type {NewContext} from './ReactFiberNewContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -56,15 +58,6 @@ import { cloneChildFibers, } from './ReactChildFiber'; import {processUpdateQueue} from './ReactFiberUpdateQueue'; -import { - getMaskedContext, - getUnmaskedContext, - hasContextChanged as hasLegacyContextChanged, - pushContextProvider as pushLegacyContextProvider, - pushTopLevelContextObject, - invalidateContextProvider, -} from './ReactFiberContext'; -import {pushProvider} from './ReactFiberNewContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -82,6 +75,8 @@ if (__DEV__) { export default function( config: HostConfig, hostContext: HostContext, + legacyContext: LegacyContext, + newContext: NewContext, hydrationContext: HydrationContext, scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, @@ -90,6 +85,17 @@ export default function( const {pushHostContext, pushHostContainer} = hostContext; + const {pushProvider} = newContext; + + const { + getMaskedContext, + getUnmaskedContext, + hasContextChanged: hasLegacyContextChanged, + pushContextProvider: pushLegacyContextProvider, + pushTopLevelContextObject, + invalidateContextProvider, + } = legacyContext; + const { enterHydrationState, resetHydrationState, @@ -104,6 +110,7 @@ export default function( resumeMountClassInstance, updateClassInstance, } = ReactFiberClassComponent( + legacyContext, scheduleWork, computeExpirationForFiber, memoizeProps, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 6d682cd2329..d8c2cd20695 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -9,6 +9,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {LegacyContext} from './ReactFiberContext'; import type {CapturedValue} from './ReactCapturedValue'; import {Update} from 'shared/ReactTypeOfSideEffect'; @@ -29,17 +30,10 @@ import warning from 'fbjs/lib/warning'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; -import { - cacheContext, - getMaskedContext, - getUnmaskedContext, - isContextConsumer, -} from './ReactFiberContext'; import { insertUpdateIntoFiber, processUpdateQueue, } from './ReactFiberUpdateQueue'; -import {hasContextChanged} from './ReactFiberContext'; const fakeInternalInstance = {}; const isArray = Array.isArray; @@ -110,11 +104,20 @@ function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array) { } export default function( + legacyContext: LegacyContext, scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, ) { + const { + cacheContext, + getMaskedContext, + getUnmaskedContext, + isContextConsumer, + hasContextChanged, + } = legacyContext; + // Class component state updater const updater = { isMounted, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9e796dd53c1..9f5ec49174e 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -11,6 +11,8 @@ import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; +import type {LegacyContext} from './ReactFiberContext'; +import type {NewContext} from './ReactFiberNewContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; @@ -45,15 +47,12 @@ import { import invariant from 'fbjs/lib/invariant'; import {reconcileChildFibers} from './ReactChildFiber'; -import { - popContextProvider as popLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, -} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; export default function( config: HostConfig, hostContext: HostContext, + legacyContext: LegacyContext, + newContext: NewContext, hydrationContext: HydrationContext, ) { const { @@ -73,6 +72,13 @@ export default function( popHostContainer, } = hostContext; + const { + popContextProvider: popLegacyContextProvider, + popTopLevelContextObject: popTopLevelLegacyContextObject, + } = legacyContext; + + const {popProvider} = newContext; + const { prepareToHydrateHostInstance, prepareToHydrateHostTextInstance, diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 7413eeaf881..8c94bc3e43a 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactFiber'; -import type {StackCursor} from './ReactFiberStack'; +import type {StackCursor, Stack} from './ReactFiberStack'; import {isFiberMounted} from 'react-reconciler/reflection'; import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; @@ -18,7 +18,6 @@ import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; import checkPropTypes from 'prop-types/checkPropTypes'; -import {createCursor, pop, push} from './ReactFiberStack'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; @@ -28,273 +27,316 @@ if (__DEV__) { warnedAboutMissingGetChildContext = {}; } -// A cursor to the current merged context object on the stack. -let contextStackCursor: StackCursor = createCursor(emptyObject); -// A cursor to a boolean indicating whether the context has changed. -let didPerformWorkStackCursor: StackCursor = createCursor(false); -// Keep track of the previous context object that was on the stack. -// We use this to get access to the parent context after we have already -// pushed the next context provider, and now need to merge their contexts. -let previousContext: Object = emptyObject; - -export function getUnmaskedContext(workInProgress: Fiber): Object { - const hasOwnContext = isContextProvider(workInProgress); - if (hasOwnContext) { - // If the fiber is a context provider itself, when we read its context - // we 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; -} - -export function cacheContext( - workInProgress: Fiber, - unmaskedContext: Object, - maskedContext: Object, -) { - const instance = workInProgress.stateNode; - instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; - instance.__reactInternalMemoizedMaskedChildContext = maskedContext; -} - -export function getMaskedContext( - workInProgress: Fiber, - unmaskedContext: Object, -) { - const type = workInProgress.type; - const contextTypes = type.contextTypes; - if (!contextTypes) { - return emptyObject; +export type LegacyContext = { + getUnmaskedContext(workInProgress: Fiber): Object, + cacheContext( + workInProgress: Fiber, + unmaskedContext: Object, + maskedContext: Object, + ): void, + getMaskedContext(workInProgress: Fiber, unmaskedContext: Object): Object, + hasContextChanged(): boolean, + isContextConsumer(fiber: Fiber): boolean, + isContextProvider(fiber: Fiber): boolean, + popContextProvider(fiber: Fiber): void, + popTopLevelContextObject(fiber: Fiber): void, + pushTopLevelContextObject( + fiber: Fiber, + context: Object, + didChange: boolean, + ): void, + processChildContext(fiber: Fiber, parentContext: Object): Object, + pushContextProvider(workInProgress: Fiber): boolean, + invalidateContextProvider(workInProgress: Fiber, didChange: boolean): void, + resetContext(): void, + findCurrentUnmaskedContext(fiber: Fiber): Object, +}; + +export default function(stack: Stack): LegacyContext { + const {createCursor, push, pop} = stack; + + // A cursor to the current merged context object on the stack. + let contextStackCursor: StackCursor = createCursor(emptyObject); + // A cursor to a boolean indicating whether the context has changed. + let didPerformWorkStackCursor: StackCursor = createCursor(false); + // Keep track of the previous context object that was on the stack. + // We use this to get access to the parent context after we have already + // pushed the next context provider, and now need to merge their contexts. + let previousContext: Object = emptyObject; + + function getUnmaskedContext(workInProgress: Fiber): Object { + const hasOwnContext = isContextProvider(workInProgress); + if (hasOwnContext) { + // If the fiber is a context provider itself, when we read its context + // we 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; } - // 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 + function cacheContext( + workInProgress: Fiber, + unmaskedContext: Object, + maskedContext: Object, ) { - return instance.__reactInternalMemoizedMaskedChildContext; - } - - const context = {}; - for (let key in contextTypes) { - context[key] = unmaskedContext[key]; + const instance = workInProgress.stateNode; + instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; + instance.__reactInternalMemoizedMaskedChildContext = maskedContext; } - if (__DEV__) { - const name = getComponentName(workInProgress) || 'Unknown'; - checkPropTypes( - contextTypes, - context, - 'context', - name, - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); - } - - // 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); - } + function getMaskedContext(workInProgress: Fiber, unmaskedContext: Object) { + const type = workInProgress.type; + const contextTypes = type.contextTypes; + if (!contextTypes) { + return emptyObject; + } - return context; -} + // 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; + } -export function hasContextChanged(): boolean { - return didPerformWorkStackCursor.current; -} + const context = {}; + for (let key in contextTypes) { + context[key] = unmaskedContext[key]; + } -export function isContextConsumer(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.contextTypes != null; -} + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Unknown'; + checkPropTypes( + contextTypes, + context, + 'context', + name, + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); + } -export function isContextProvider(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; -} + // 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); + } -export function popContextProvider(fiber: Fiber): void { - if (!isContextProvider(fiber)) { - return; + return context; } - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); -} - -export function popTopLevelContextObject(fiber: Fiber) { - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); -} + function hasContextChanged(): boolean { + return didPerformWorkStackCursor.current; + } -export function pushTopLevelContextObject( - fiber: Fiber, - context: Object, - didChange: boolean, -): void { - invariant( - contextStackCursor.cursor == null, - 'Unexpected context found on stack. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - push(contextStackCursor, context, fiber); - push(didPerformWorkStackCursor, didChange, fiber); -} + function isContextConsumer(fiber: Fiber): boolean { + return fiber.tag === ClassComponent && fiber.type.contextTypes != null; + } -export function processChildContext( - fiber: Fiber, - parentContext: Object, -): Object { - const instance = fiber.stateNode; - const childContextTypes = fiber.type.childContextTypes; + function isContextProvider(fiber: Fiber): boolean { + return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; + } - // TODO (bvaughn) Replace this behavior with an invariant() in the future. - // It has only been added in Fiber to match the (unintentional) behavior in Stack. - if (typeof instance.getChildContext !== 'function') { - if (__DEV__) { - const componentName = getComponentName(fiber) || 'Unknown'; - - if (!warnedAboutMissingGetChildContext[componentName]) { - warnedAboutMissingGetChildContext[componentName] = true; - warning( - false, - '%s.childContextTypes is specified but there is no getChildContext() method ' + - 'on the instance. You can either define getChildContext() on %s or remove ' + - 'childContextTypes from it.', - componentName, - componentName, - ); - } + function popContextProvider(fiber: Fiber): void { + if (!isContextProvider(fiber)) { + return; } - return parentContext; - } - let childContext; - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); } - startPhaseTimer(fiber, 'getChildContext'); - childContext = instance.getChildContext(); - stopPhaseTimer(); - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase(null); + + function popTopLevelContextObject(fiber: Fiber) { + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); } - for (let contextKey in childContext) { + + function pushTopLevelContextObject( + fiber: Fiber, + context: Object, + didChange: boolean, + ): void { invariant( - contextKey in childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - getComponentName(fiber) || 'Unknown', - contextKey, - ); - } - if (__DEV__) { - const name = getComponentName(fiber) || 'Unknown'; - checkPropTypes( - childContextTypes, - childContext, - 'child context', - name, - // In practice, there is one case in which we won't get a stack. It's when - // somebody calls unstable_renderSubtreeIntoContainer() and we process - // context from the parent component instance. The stack will be missing - // because it's outside of the reconciliation, and so the pointer has not - // been set. This is rare and doesn't matter. We'll also remove that API. - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + contextStackCursor.cursor == null, + 'Unexpected context found on stack. ' + + 'This error is likely caused by a bug in React. Please file an issue.', ); + + push(contextStackCursor, context, fiber); + push(didPerformWorkStackCursor, didChange, fiber); } - return {...parentContext, ...childContext}; -} + function processChildContext(fiber: Fiber, parentContext: Object): Object { + const instance = fiber.stateNode; + const childContextTypes = fiber.type.childContextTypes; + + // TODO (bvaughn) Replace this behavior with an invariant() in the future. + // It has only been added in Fiber to match the (unintentional) behavior in Stack. + if (typeof instance.getChildContext !== 'function') { + if (__DEV__) { + const componentName = getComponentName(fiber) || 'Unknown'; + + if (!warnedAboutMissingGetChildContext[componentName]) { + warnedAboutMissingGetChildContext[componentName] = true; + warning( + false, + '%s.childContextTypes is specified but there is no getChildContext() method ' + + 'on the instance. You can either define getChildContext() on %s or remove ' + + 'childContextTypes from it.', + componentName, + componentName, + ); + } + } + return parentContext; + } -export function pushContextProvider(workInProgress: Fiber): boolean { - if (!isContextProvider(workInProgress)) { - return false; + let childContext; + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); + } + startPhaseTimer(fiber, 'getChildContext'); + childContext = instance.getChildContext(); + stopPhaseTimer(); + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase(null); + } + for (let contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(fiber) || 'Unknown', + contextKey, + ); + } + if (__DEV__) { + const name = getComponentName(fiber) || 'Unknown'; + checkPropTypes( + childContextTypes, + childContext, + 'child context', + name, + // In practice, there is one case in which we won't get a stack. It's when + // somebody calls unstable_renderSubtreeIntoContainer() and we process + // context from the parent component instance. The stack will be missing + // because it's outside of the reconciliation, and so the pointer has not + // been set. This is rare and doesn't matter. We'll also remove that API. + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); + } + + return {...parentContext, ...childContext}; } - const instance = workInProgress.stateNode; - // We push the context as early as possible to ensure stack integrity. - // If the instance does not exist yet, we will push null at first, - // and replace it on the stack later when invalidating the context. - const memoizedMergedChildContext = - (instance && instance.__reactInternalMemoizedMergedChildContext) || - emptyObject; - - // Remember the parent context so we can merge with it later. - // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. - previousContext = contextStackCursor.current; - push(contextStackCursor, memoizedMergedChildContext, workInProgress); - push( - didPerformWorkStackCursor, - didPerformWorkStackCursor.current, - workInProgress, - ); - - return true; -} + function pushContextProvider(workInProgress: Fiber): boolean { + if (!isContextProvider(workInProgress)) { + return false; + } + + const instance = workInProgress.stateNode; + // We push the context as early as possible to ensure stack integrity. + // If the instance does not exist yet, we will push null at first, + // and replace it on the stack later when invalidating the context. + const memoizedMergedChildContext = + (instance && instance.__reactInternalMemoizedMergedChildContext) || + emptyObject; + + // Remember the parent context so we can merge with it later. + // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. + previousContext = contextStackCursor.current; + push(contextStackCursor, memoizedMergedChildContext, workInProgress); + push( + didPerformWorkStackCursor, + didPerformWorkStackCursor.current, + workInProgress, + ); -export function invalidateContextProvider( - workInProgress: Fiber, - didChange: boolean, -): void { - const instance = workInProgress.stateNode; - invariant( - instance, - 'Expected to have an instance by this point. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - if (didChange) { - // Merge parent and own context. - // Skip this if we're not updating due to sCU. - // This avoids unnecessarily recomputing memoized values. - const mergedContext = processChildContext(workInProgress, previousContext); - instance.__reactInternalMemoizedMergedChildContext = mergedContext; - - // Replace the old (or empty) context with the new one. - // It is important to unwind the context in the reverse order. - pop(didPerformWorkStackCursor, workInProgress); - pop(contextStackCursor, workInProgress); - // Now push the new context and mark that it has changed. - push(contextStackCursor, mergedContext, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); - } else { - pop(didPerformWorkStackCursor, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); + return true; } -} -export function resetContext(): void { - previousContext = emptyObject; - contextStackCursor.current = emptyObject; - didPerformWorkStackCursor.current = false; -} + function invalidateContextProvider( + workInProgress: Fiber, + didChange: boolean, + ): void { + const instance = workInProgress.stateNode; + invariant( + instance, + 'Expected to have an instance by this point. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); -export function findCurrentUnmaskedContext(fiber: Fiber): Object { - // Currently this is only used with renderSubtreeIntoContainer; not sure if it - // makes sense elsewhere - invariant( - isFiberMounted(fiber) && fiber.tag === ClassComponent, - 'Expected subtree parent to be a mounted class component. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - let node: Fiber = fiber; - while (node.tag !== HostRoot) { - if (isContextProvider(node)) { - return node.stateNode.__reactInternalMemoizedMergedChildContext; + if (didChange) { + // Merge parent and own context. + // Skip this if we're not updating due to sCU. + // This avoids unnecessarily recomputing memoized values. + const mergedContext = processChildContext( + workInProgress, + previousContext, + ); + instance.__reactInternalMemoizedMergedChildContext = mergedContext; + + // Replace the old (or empty) context with the new one. + // It is important to unwind the context in the reverse order. + pop(didPerformWorkStackCursor, workInProgress); + pop(contextStackCursor, workInProgress); + // Now push the new context and mark that it has changed. + push(contextStackCursor, mergedContext, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); + } else { + pop(didPerformWorkStackCursor, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); } - const parent = node.return; + } + + function resetContext(): void { + previousContext = emptyObject; + contextStackCursor.current = emptyObject; + didPerformWorkStackCursor.current = false; + } + + function findCurrentUnmaskedContext(fiber: Fiber): Object { + // Currently this is only used with renderSubtreeIntoContainer; not sure if it + // makes sense elsewhere invariant( - parent, - 'Found unexpected detached subtree parent. ' + + isFiberMounted(fiber) && fiber.tag === ClassComponent, + 'Expected subtree parent to be a mounted class component. ' + 'This error is likely caused by a bug in React. Please file an issue.', ); - node = parent; + + let node: Fiber = fiber; + while (node.tag !== HostRoot) { + if (isContextProvider(node)) { + return node.stateNode.__reactInternalMemoizedMergedChildContext; + } + const parent = node.return; + invariant( + parent, + 'Found unexpected detached subtree parent. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + node = parent; + } + return node.stateNode.context; } - return node.stateNode.context; + + return { + getUnmaskedContext, + cacheContext, + getMaskedContext, + hasContextChanged, + isContextConsumer, + isContextProvider, + popContextProvider, + popTopLevelContextObject, + pushTopLevelContextObject, + processChildContext, + pushContextProvider, + invalidateContextProvider, + resetContext, + findCurrentUnmaskedContext, + }; } diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 7bb479b821f..e505b479bfe 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -9,12 +9,10 @@ import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; -import type {StackCursor} from './ReactFiberStack'; +import type {StackCursor, Stack} from './ReactFiberStack'; import invariant from 'fbjs/lib/invariant'; -import {createCursor, pop, push} from './ReactFiberStack'; - declare class NoContextT {} const NO_CONTEXT: NoContextT = ({}: any); @@ -30,8 +28,10 @@ export type HostContext = { export default function( config: HostConfig, + stack: Stack, ): HostContext { const {getChildHostContext, getRootHostContext} = config; + const {createCursor, push, pop} = stack; let contextStackCursor: StackCursor = createCursor( NO_CONTEXT, diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 3c8bf67fd01..38fcdc220fb 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -12,64 +12,78 @@ import type {ReactContext} from 'shared/ReactTypes'; import warning from 'fbjs/lib/warning'; -let changedBitsStack: Array = []; -let currentValueStack: Array = []; -let stack: Array = []; -let index = -1; +export type NewContext = { + pushProvider(providerFiber: Fiber): void, + popProvider(providerFiber: Fiber): void, + resetProviderStack(): void, +}; -let rendererSigil; -if (__DEV__) { - // Use this to detect multiple renderers using the same context - rendererSigil = {}; -} - -export function pushProvider(providerFiber: Fiber): void { - const context: ReactContext = providerFiber.type.context; - index += 1; - changedBitsStack[index] = context._changedBits; - currentValueStack[index] = context._currentValue; - stack[index] = providerFiber; - context._currentValue = providerFiber.pendingProps.value; - context._changedBits = providerFiber.stateNode; +export default function() { + let changedBitsStack: Array = []; + let currentValueStack: Array = []; + let stack: Array = []; + let index = -1; + let rendererSigil; if (__DEV__) { - warning( - context._currentRenderer === null || - context._currentRenderer === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer = rendererSigil; + // Use this to detect multiple renderers using the same context + rendererSigil = {}; } -} -export function popProvider(providerFiber: Fiber): void { - if (__DEV__) { - warning(index > -1 && providerFiber === stack[index], 'Unexpected pop.'); + function pushProvider(providerFiber: Fiber): void { + const context: ReactContext = providerFiber.type.context; + index += 1; + changedBitsStack[index] = context._changedBits; + currentValueStack[index] = context._currentValue; + stack[index] = providerFiber; + context._currentValue = providerFiber.pendingProps.value; + context._changedBits = providerFiber.stateNode; + + if (__DEV__) { + warning( + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer = rendererSigil; + } } - const changedBits = changedBitsStack[index]; - const currentValue = currentValueStack[index]; - changedBitsStack[index] = null; - currentValueStack[index] = null; - stack[index] = null; - index -= 1; - const context: ReactContext = providerFiber.type.context; - context._currentValue = currentValue; - context._changedBits = changedBits; -} -export function resetProviderStack(): void { - for (let i = index; i > -1; i--) { - const providerFiber = stack[i]; - const context: ReactContext = providerFiber.type.context; - context._currentValue = context._defaultValue; - context._changedBits = 0; - changedBitsStack[i] = null; - currentValueStack[i] = null; - stack[i] = null; + function popProvider(providerFiber: Fiber): void { if (__DEV__) { - context._currentRenderer = null; + warning(index > -1 && providerFiber === stack[index], 'Unexpected pop.'); } + const changedBits = changedBitsStack[index]; + const currentValue = currentValueStack[index]; + changedBitsStack[index] = null; + currentValueStack[index] = null; + stack[index] = null; + index -= 1; + const context: ReactContext = providerFiber.type.context; + context._currentValue = currentValue; + context._changedBits = changedBits; } - index = -1; + + function resetProviderStack(): void { + for (let i = index; i > -1; i--) { + const providerFiber = stack[i]; + const context: ReactContext = providerFiber.type.context; + context._currentValue = context._defaultValue; + context._changedBits = 0; + changedBitsStack[i] = null; + currentValueStack[i] = null; + stack[i] = null; + if (__DEV__) { + context._currentRenderer = null; + } + } + index = -1; + } + + return { + pushProvider, + popProvider, + resetProviderStack, + }; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 949e8067794..bb13271f060 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -22,11 +22,6 @@ import emptyObject from 'fbjs/lib/emptyObject'; import getComponentName from 'shared/getComponentName'; import warning from 'fbjs/lib/warning'; -import { - findCurrentUnmaskedContext, - isContextProvider, - processChildContext, -} from './ReactFiberContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import ReactFiberScheduler from './ReactFiberScheduler'; @@ -274,20 +269,6 @@ export type Reconciler = { findHostInstanceWithNoPortals(component: Fiber): I | TI | null, }; -function getContextForSubtree( - parentComponent: ?React$Component, -): Object { - if (!parentComponent) { - return emptyObject; - } - - const fiber = ReactInstanceMap.get(parentComponent); - const parentContext = findCurrentUnmaskedContext(fiber); - return isContextProvider(fiber) - ? processChildContext(fiber, parentContext) - : parentContext; -} - export default function( config: HostConfig, ): Reconciler { @@ -308,8 +289,29 @@ export default function( syncUpdates, interactiveUpdates, flushInteractiveUpdates, + legacyContext, } = ReactFiberScheduler(config); + const { + findCurrentUnmaskedContext, + isContextProvider, + processChildContext, + } = legacyContext; + + function getContextForSubtree( + parentComponent: ?React$Component, + ): Object { + if (!parentComponent) { + return emptyObject; + } + + const fiber = ReactInstanceMap.get(parentComponent); + const parentContext = findCurrentUnmaskedContext(fiber); + return isContextProvider(fiber) + ? processChildContext(fiber, parentContext) + : parentContext; + } + function scheduleRootUpdate( current: Fiber, element: ReactNodeList, diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 09fb2679e4e..09906c17f25 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -72,7 +72,6 @@ import { startCommitLifeCyclesTimer, stopCommitLifeCyclesTimer, } from './ReactDebugFiberPerf'; -import {reset} from './ReactFiberStack'; import {createWorkInProgress} from './ReactFiber'; import {onCommitRoot} from './ReactFiberDevToolsHook'; import { @@ -84,18 +83,14 @@ import { computeExpirationBucket, } from './ReactFiberExpirationTime'; import {AsyncMode} from './ReactTypeOfMode'; -import { - resetContext as resetLegacyContext, - popContextProvider as popLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, -} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; -import {resetProviderStack} from './ReactFiberNewContext'; +import ReactFiberLegacyContext from './ReactFiberContext'; +import ReactFiberNewContext from './ReactFiberNewContext'; import { getUpdateExpirationTime, insertUpdateIntoFiber, } from './ReactFiberUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; +import ReactFiberStack from './ReactFiberStack'; const { invokeGuardedCallback, @@ -161,8 +156,18 @@ if (__DEV__) { export default function( config: HostConfig, ) { - const hostContext = ReactFiberHostContext(config); + const stack = ReactFiberStack(); + const {reset: resetStack} = stack; + const hostContext = ReactFiberHostContext(config, stack); + const legacyContext = ReactFiberLegacyContext(stack); + const newContext = ReactFiberNewContext(); const {popHostContext, popHostContainer} = hostContext; + const { + popTopLevelContextObject: popTopLevelLegacyContextObject, + popContextProvider: popLegacyContextProvider, + resetContext: resetLegacyContext, + } = legacyContext; + const {popProvider, resetProviderStack} = newContext; const hydrationContext: HydrationContext = ReactFiberHydrationContext( config, ); @@ -170,6 +175,8 @@ export default function( const {beginWork} = ReactFiberBeginWork( config, hostContext, + legacyContext, + newContext, hydrationContext, scheduleWork, computeExpirationForFiber, @@ -177,10 +184,14 @@ export default function( const {completeWork} = ReactFiberCompleteWork( config, hostContext, + legacyContext, + newContext, hydrationContext, ); const {throwException, unwindWork} = ReactFiberUnwindWork( hostContext, + legacyContext, + newContext, scheduleWork, isAlreadyFailedLegacyErrorBoundary, ); @@ -280,7 +291,7 @@ export default function( function resetContextStack() { // Reset the stack - reset(); + resetStack(); // Reset the cursors resetLegacyContext(); resetHostContainer(); @@ -1679,5 +1690,6 @@ export default function( interactiveUpdates, flushInteractiveUpdates, computeUniqueAsyncExpiration, + legacyContext, }; } diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index c15214f267d..8dc9a38b76b 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -15,71 +15,89 @@ export type StackCursor = { current: T, }; -const valueStack: Array = []; +export type Stack = { + createCursor(defaultValue: T): StackCursor, + isEmpty(): boolean, + push(cursor: StackCursor, value: T, fiber: Fiber): void, + pop(cursor: StackCursor, fiber: Fiber): void, + reset(): void, +}; -let fiberStack: Array; +export default function(): Stack { + const valueStack: Array = []; -if (__DEV__) { - fiberStack = []; -} + let fiberStack: Array; -let index = -1; + if (__DEV__) { + fiberStack = []; + } -export function createCursor(defaultValue: T): StackCursor { - return { - current: defaultValue, - }; -} + let index = -1; -export function isEmpty(): boolean { - return index === -1; -} + function createCursor(defaultValue: T): StackCursor { + return { + current: defaultValue, + }; + } -export function pop(cursor: StackCursor, fiber: Fiber): void { - if (index < 0) { - if (__DEV__) { - warning(false, 'Unexpected pop.'); - } - return; + function isEmpty(): boolean { + return index === -1; } - if (__DEV__) { - if (fiber !== fiberStack[index]) { - warning(false, 'Unexpected Fiber popped.'); + function pop(cursor: StackCursor, fiber: Fiber): void { + if (index < 0) { + if (__DEV__) { + warning(false, 'Unexpected pop.'); + } + return; } - } - cursor.current = valueStack[index]; + if (__DEV__) { + if (fiber !== fiberStack[index]) { + warning(false, 'Unexpected Fiber popped.'); + } + } - valueStack[index] = null; + cursor.current = valueStack[index]; - if (__DEV__) { - fiberStack[index] = null; + valueStack[index] = null; + + if (__DEV__) { + fiberStack[index] = null; + } + + index--; } - index--; -} + function push(cursor: StackCursor, value: T, fiber: Fiber): void { + index++; -export function push(cursor: StackCursor, value: T, fiber: Fiber): void { - index++; + valueStack[index] = cursor.current; - valueStack[index] = cursor.current; + if (__DEV__) { + fiberStack[index] = fiber; + } - if (__DEV__) { - fiberStack[index] = fiber; + cursor.current = value; } - cursor.current = value; -} + function reset(): void { + while (index > -1) { + valueStack[index] = null; -export function reset(): void { - while (index > -1) { - valueStack[index] = null; + if (__DEV__) { + fiberStack[index] = null; + } - if (__DEV__) { - fiberStack[index] = null; + index--; } - - index--; } + + return { + createCursor, + isEmpty, + pop, + push, + reset, + }; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index bb3ce84945d..e4d348f8fc1 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -4,8 +4,16 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow */ +import type {Fiber} from './ReactFiber'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {HostContext} from './ReactFiberHostContext'; +import type {LegacyContext} from './ReactFiberContext'; +import type {NewContext} from './ReactFiberNewContext'; +import type {UpdateQueue} from './ReactFiberUpdateQueue'; + import {createCapturedValue} from './ReactCapturedValue'; import {ensureUpdateQueues} from './ReactFiberUpdateQueue'; @@ -25,14 +33,10 @@ import { import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags'; -import { - popContextProvider as popLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, -} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; - -export default function( +export default function( hostContext: HostContext, + legacyContext: LegacyContext, + newContext: NewContext, scheduleWork: ( fiber: Fiber, startTime: ExpirationTime, @@ -41,6 +45,11 @@ export default function( isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean, ) { const {popHostContainer, popHostContext} = hostContext; + const { + popContextProvider: popLegacyContextProvider, + popTopLevelContextObject: popTopLevelLegacyContextObject, + } = legacyContext; + const {popProvider} = newContext; function throwException( returnFiber: Fiber, @@ -61,7 +70,9 @@ export default function( // Uncaught error const errorInfo = value; ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + const updateQueue: UpdateQueue< + any, + > = (workInProgress.updateQueue: any); updateQueue.capturedValues = [errorInfo]; workInProgress.effectTag |= ShouldCapture; return; @@ -79,7 +90,9 @@ export default function( !isAlreadyFailedLegacyErrorBoundary(instance))) ) { ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + const updateQueue: UpdateQueue< + any, + > = (workInProgress.updateQueue: any); const capturedValues = updateQueue.capturedValues; if (capturedValues === null) { updateQueue.capturedValues = [value]; @@ -97,7 +110,7 @@ export default function( } while (workInProgress !== null); } - function unwindWork(workInProgress) { + function unwindWork(workInProgress: Fiber) { switch (workInProgress.tag) { case ClassComponent: { popLegacyContextProvider(workInProgress); From 3bb28ba7c9f8e89e17c43d4ce7cf0677dfc38a02 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 12 Mar 2018 17:06:21 -0700 Subject: [PATCH 2/3] Unify context implementations Implements the new context API on top of the existing ReactStack that we already use for host context and legacy context. Now there is a single array that we push and pop from. This makes the interrupt path slightly slower, since when we reset the unit of work pointer, we have to iterate over the stack (like before) *and* switch on the type of work (not like before). On the other hand, this unifies all of the unwinding behavior in the UnwindWork module. --- .../react-reconciler/src/ReactFiberContext.js | 8 --- .../src/ReactFiberHostContext.js | 7 --- .../src/ReactFiberNewContext.js | 55 +++++++------------ .../src/ReactFiberScheduler.js | 34 ++++++------ .../react-reconciler/src/ReactFiberStack.js | 14 ----- .../src/ReactFiberUnwindWork.js | 28 ++++++++++ .../ReactIncrementalTriangle-test.internal.js | 7 +++ 7 files changed, 71 insertions(+), 82 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 8c94bc3e43a..5db08417035 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -48,7 +48,6 @@ export type LegacyContext = { processChildContext(fiber: Fiber, parentContext: Object): Object, pushContextProvider(workInProgress: Fiber): boolean, invalidateContextProvider(workInProgress: Fiber, didChange: boolean): void, - resetContext(): void, findCurrentUnmaskedContext(fiber: Fiber): Object, }; @@ -292,12 +291,6 @@ export default function(stack: Stack): LegacyContext { } } - function resetContext(): void { - previousContext = emptyObject; - contextStackCursor.current = emptyObject; - didPerformWorkStackCursor.current = false; - } - function findCurrentUnmaskedContext(fiber: Fiber): Object { // Currently this is only used with renderSubtreeIntoContainer; not sure if it // makes sense elsewhere @@ -336,7 +329,6 @@ export default function(stack: Stack): LegacyContext { processChildContext, pushContextProvider, invalidateContextProvider, - resetContext, findCurrentUnmaskedContext, }; } diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index e505b479bfe..f58651381a2 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -23,7 +23,6 @@ export type HostContext = { popHostContext(fiber: Fiber): void, pushHostContainer(fiber: Fiber, container: C): void, pushHostContext(fiber: Fiber): void, - resetHostContainer(): void, }; export default function( @@ -108,11 +107,6 @@ export default function( pop(contextFiberStackCursor, fiber); } - function resetHostContainer() { - contextStackCursor.current = NO_CONTEXT; - rootInstanceStackCursor.current = NO_CONTEXT; - } - return { getHostContext, getRootHostContainer, @@ -120,6 +114,5 @@ export default function( popHostContext, pushHostContainer, pushHostContext, - resetHostContainer, }; } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 38fcdc220fb..5f64352438e 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -9,20 +9,21 @@ import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; +import type {StackCursor, Stack} from './ReactFiberStack'; import warning from 'fbjs/lib/warning'; export type NewContext = { pushProvider(providerFiber: Fiber): void, popProvider(providerFiber: Fiber): void, - resetProviderStack(): void, }; -export default function() { - let changedBitsStack: Array = []; - let currentValueStack: Array = []; - let stack: Array = []; - let index = -1; +export default function(stack: Stack) { + const {createCursor, push, pop} = stack; + + const providerCursor: StackCursor = createCursor(null); + const valueCursor: StackCursor = createCursor(null); + const changedBitsCursor: StackCursor = createCursor(0); let rendererSigil; if (__DEV__) { @@ -32,10 +33,11 @@ export default function() { function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type.context; - index += 1; - changedBitsStack[index] = context._changedBits; - currentValueStack[index] = context._currentValue; - stack[index] = providerFiber; + + push(changedBitsCursor, context._changedBits, providerFiber); + push(valueCursor, context._currentValue, providerFiber); + push(providerCursor, providerFiber, providerFiber); + context._currentValue = providerFiber.pendingProps.value; context._changedBits = providerFiber.stateNode; @@ -51,39 +53,20 @@ export default function() { } function popProvider(providerFiber: Fiber): void { - if (__DEV__) { - warning(index > -1 && providerFiber === stack[index], 'Unexpected pop.'); - } - const changedBits = changedBitsStack[index]; - const currentValue = currentValueStack[index]; - changedBitsStack[index] = null; - currentValueStack[index] = null; - stack[index] = null; - index -= 1; + const changedBits = changedBitsCursor.current; + const currentValue = valueCursor.current; + + pop(providerCursor, providerFiber); + pop(valueCursor, providerFiber); + pop(changedBitsCursor, providerFiber); + const context: ReactContext = providerFiber.type.context; context._currentValue = currentValue; context._changedBits = changedBits; } - function resetProviderStack(): void { - for (let i = index; i > -1; i--) { - const providerFiber = stack[i]; - const context: ReactContext = providerFiber.type.context; - context._currentValue = context._defaultValue; - context._changedBits = 0; - changedBitsStack[i] = null; - currentValueStack[i] = null; - stack[i] = null; - if (__DEV__) { - context._currentRenderer = null; - } - } - index = -1; - } - return { pushProvider, popProvider, - resetProviderStack, }; } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 09906c17f25..63ed716cd77 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -157,21 +157,18 @@ export default function( config: HostConfig, ) { const stack = ReactFiberStack(); - const {reset: resetStack} = stack; const hostContext = ReactFiberHostContext(config, stack); const legacyContext = ReactFiberLegacyContext(stack); - const newContext = ReactFiberNewContext(); + const newContext = ReactFiberNewContext(stack); const {popHostContext, popHostContainer} = hostContext; const { popTopLevelContextObject: popTopLevelLegacyContextObject, popContextProvider: popLegacyContextProvider, - resetContext: resetLegacyContext, } = legacyContext; - const {popProvider, resetProviderStack} = newContext; + const {popProvider} = newContext; const hydrationContext: HydrationContext = ReactFiberHydrationContext( config, ); - const {resetHostContainer} = hostContext; const {beginWork} = ReactFiberBeginWork( config, hostContext, @@ -188,7 +185,11 @@ export default function( newContext, hydrationContext, ); - const {throwException, unwindWork} = ReactFiberUnwindWork( + const { + throwException, + unwindWork, + unwindInterruptedWork, + } = ReactFiberUnwindWork( hostContext, legacyContext, newContext, @@ -289,15 +290,14 @@ export default function( }; } - function resetContextStack() { - // Reset the stack - resetStack(); - // Reset the cursors - resetLegacyContext(); - resetHostContainer(); - - // TODO: Unify new context implementation with other stacks - resetProviderStack(); + function resetStack() { + if (nextUnitOfWork !== null) { + let interruptedWork = nextUnitOfWork.return; + while (interruptedWork !== null) { + unwindInterruptedWork(interruptedWork); + interruptedWork = interruptedWork.return; + } + } if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); @@ -841,7 +841,7 @@ export default function( nextUnitOfWork === null ) { // Reset the stack and start working from the root. - resetContextStack(); + resetStack(); nextRoot = root; nextRenderExpirationTime = expirationTime; nextUnitOfWork = createWorkInProgress( @@ -1102,7 +1102,7 @@ export default function( ) { // This is an interruption. (Used for performance tracking.) interruptedBy = fiber; - resetContextStack(); + resetStack(); } if (nextRoot !== root || !isWorking) { requestWork(root, expirationTime); diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index 8dc9a38b76b..c99d610e391 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -20,7 +20,6 @@ export type Stack = { isEmpty(): boolean, push(cursor: StackCursor, value: T, fiber: Fiber): void, pop(cursor: StackCursor, fiber: Fiber): void, - reset(): void, }; export default function(): Stack { @@ -81,23 +80,10 @@ export default function(): Stack { cursor.current = value; } - function reset(): void { - while (index > -1) { - valueStack[index] = null; - - if (__DEV__) { - fiberStack[index] = null; - } - - index--; - } - } - return { createCursor, isEmpty, pop, push, - reset, }; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index e4d348f8fc1..6565e4888df 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -145,8 +145,36 @@ export default function( return null; } } + + function unwindInterruptedWork(interruptedWork: Fiber) { + switch (interruptedWork.tag) { + case ClassComponent: { + popLegacyContextProvider(interruptedWork); + break; + } + case HostRoot: { + popHostContainer(interruptedWork); + popTopLevelLegacyContextObject(interruptedWork); + break; + } + case HostComponent: { + popHostContext(interruptedWork); + break; + } + case HostPortal: + popHostContainer(interruptedWork); + break; + case ContextProvider: + popProvider(interruptedWork); + break; + default: + break; + } + } + return { throwException, unwindWork, + unwindInterruptedWork, }; } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js index 283c7dc4f8f..628e1c38806 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js @@ -542,6 +542,13 @@ ${formatActions(actions)} ['c', step(2)], ['b', interrupt()], ); + + simulateMultipleRoots( + ['c', toggle(0)], + ['c', step(1)], + ['b', flush(7)], + ['c', toggle(0)], + ); }); it('generative tests', () => { From 3ae3d457b3712e4848a974b740ddedd56e31a394 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 15 Mar 2018 18:14:13 -0700 Subject: [PATCH 3/3] Add DEV only warning if stack is not reset properly --- .../src/ReactFiberScheduler.js | 4 +++ .../react-reconciler/src/ReactFiberStack.js | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 63ed716cd77..ea5df6dc841 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -301,6 +301,7 @@ export default function( if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); + stack.checkThatStackIsEmpty(); } nextRoot = null; @@ -894,6 +895,9 @@ export default function( // Yield back to main thread. if (didFatal) { // There was a fatal error. + if (__DEV__) { + stack.resetStackAfterFatalErrorInDev(); + } return null; } else if (nextUnitOfWork === null) { // We reached the root. diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index c99d610e391..abf2a475b1a 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -20,6 +20,10 @@ export type Stack = { isEmpty(): boolean, push(cursor: StackCursor, value: T, fiber: Fiber): void, pop(cursor: StackCursor, fiber: Fiber): void, + + // DEV only + checkThatStackIsEmpty(): void, + resetStackAfterFatalErrorInDev(): void, }; export default function(): Stack { @@ -80,10 +84,31 @@ export default function(): Stack { cursor.current = value; } + function checkThatStackIsEmpty() { + if (__DEV__) { + if (index !== -1) { + warning( + false, + 'Expected an empty stack. Something was not reset properly.', + ); + } + } + } + + function resetStackAfterFatalErrorInDev() { + if (__DEV__) { + index = -1; + valueStack.length = 0; + fiberStack.length = 0; + } + } + return { createCursor, isEmpty, pop, push, + checkThatStackIsEmpty, + resetStackAfterFatalErrorInDev, }; }