From 208b490ed907346ae3e37159535299899f74312d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 15 Mar 2018 19:27:44 -0700 Subject: [PATCH] Unify context stack implementations (#12359) * Use module pattern so context stack is isolated per renderer * 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. * Add DEV only warning if stack is not reset properly --- .../src/ReactFiberBeginWork.js | 25 +- .../src/ReactFiberClassComponent.js | 17 +- .../src/ReactFiberCompleteWork.js | 16 +- .../react-reconciler/src/ReactFiberContext.js | 506 ++++++++++-------- .../src/ReactFiberHostContext.js | 13 +- .../src/ReactFiberNewContext.js | 97 ++-- .../src/ReactFiberReconciler.js | 40 +- .../src/ReactFiberScheduler.js | 60 ++- .../react-reconciler/src/ReactFiberStack.js | 117 ++-- .../src/ReactFiberUnwindWork.js | 61 ++- .../ReactIncrementalTriangle-test.internal.js | 7 + 11 files changed, 547 insertions(+), 412 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 039fe24032e76..1b2b799fe13ba 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'; @@ -57,15 +59,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'; @@ -83,6 +76,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, @@ -91,6 +86,17 @@ export default function( const {pushHostContext, pushHostContainer} = hostContext; + const {pushProvider} = newContext; + + const { + getMaskedContext, + getUnmaskedContext, + hasContextChanged: hasLegacyContextChanged, + pushContextProvider: pushLegacyContextProvider, + pushTopLevelContextObject, + invalidateContextProvider, + } = legacyContext; + const { enterHydrationState, resetHydrationState, @@ -105,6 +111,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 bf8abba92d2f8..7f0514f6e413a 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 3fb99ef2c2767..b1685704f8ec2 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'; @@ -46,15 +48,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 { @@ -74,6 +73,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 7413eeaf88166..5db08417035f4 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,308 @@ 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, + 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; + } + + 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, + ); + } -export function pushContextProvider(workInProgress: Fiber): boolean { - if (!isContextProvider(workInProgress)) { - return false; + 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 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, + findCurrentUnmaskedContext, + }; } diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 7bb479b821ff2..f58651381a28c 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); @@ -25,13 +23,14 @@ export type HostContext = { popHostContext(fiber: Fiber): void, pushHostContainer(fiber: Fiber, container: C): void, pushHostContext(fiber: Fiber): void, - resetHostContainer(): void, }; export default function( config: HostConfig, + stack: Stack, ): HostContext { const {getChildHostContext, getRootHostContext} = config; + const {createCursor, push, pop} = stack; let contextStackCursor: StackCursor = createCursor( NO_CONTEXT, @@ -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 3c8bf67fd01e2..5f64352438ef4 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -9,67 +9,64 @@ import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; +import type {StackCursor, Stack} from './ReactFiberStack'; 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, +}; -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(stack: Stack) { + const {createCursor, push, pop} = stack; - 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 providerCursor: StackCursor = createCursor(null); + const valueCursor: StackCursor = createCursor(null); + const changedBitsCursor: StackCursor = createCursor(0); -export function popProvider(providerFiber: Fiber): void { + let rendererSigil; if (__DEV__) { - warning(index > -1 && providerFiber === stack[index], 'Unexpected pop.'); + // Use this to detect multiple renderers using the same context + 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]; + function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type.context; - context._currentValue = context._defaultValue; - context._changedBits = 0; - changedBitsStack[i] = null; - currentValueStack[i] = null; - stack[i] = null; + + push(changedBitsCursor, context._changedBits, providerFiber); + push(valueCursor, context._currentValue, providerFiber); + push(providerCursor, providerFiber, providerFiber); + + context._currentValue = providerFiber.pendingProps.value; + context._changedBits = providerFiber.stateNode; + if (__DEV__) { - context._currentRenderer = null; + warning( + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer = rendererSigil; } } - index = -1; + + function popProvider(providerFiber: Fiber): void { + 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; + } + + return { + pushProvider, + popProvider, + }; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 949e806779418..bb13271f0603d 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 09fb2679e4e79..ea5df6dc84128 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,15 +156,24 @@ if (__DEV__) { export default function( config: HostConfig, ) { - const hostContext = ReactFiberHostContext(config); + const stack = ReactFiberStack(); + const hostContext = ReactFiberHostContext(config, stack); + const legacyContext = ReactFiberLegacyContext(stack); + const newContext = ReactFiberNewContext(stack); const {popHostContext, popHostContainer} = hostContext; + const { + popTopLevelContextObject: popTopLevelLegacyContextObject, + popContextProvider: popLegacyContextProvider, + } = legacyContext; + const {popProvider} = newContext; const hydrationContext: HydrationContext = ReactFiberHydrationContext( config, ); - const {resetHostContainer} = hostContext; const {beginWork} = ReactFiberBeginWork( config, hostContext, + legacyContext, + newContext, hydrationContext, scheduleWork, computeExpirationForFiber, @@ -177,10 +181,18 @@ export default function( const {completeWork} = ReactFiberCompleteWork( config, hostContext, + legacyContext, + newContext, hydrationContext, ); - const {throwException, unwindWork} = ReactFiberUnwindWork( + const { + throwException, + unwindWork, + unwindInterruptedWork, + } = ReactFiberUnwindWork( hostContext, + legacyContext, + newContext, scheduleWork, isAlreadyFailedLegacyErrorBoundary, ); @@ -278,18 +290,18 @@ export default function( }; } - function resetContextStack() { - // Reset the stack - reset(); - // 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(); + stack.checkThatStackIsEmpty(); } nextRoot = null; @@ -830,7 +842,7 @@ export default function( nextUnitOfWork === null ) { // Reset the stack and start working from the root. - resetContextStack(); + resetStack(); nextRoot = root; nextRenderExpirationTime = expirationTime; nextUnitOfWork = createWorkInProgress( @@ -883,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. @@ -1091,7 +1106,7 @@ export default function( ) { // This is an interruption. (Used for performance tracking.) interruptedBy = fiber; - resetContextStack(); + resetStack(); } if (nextRoot !== root || !isWorking) { requestWork(root, expirationTime); @@ -1679,5 +1694,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 c15214f267d43..abf2a475b1ae5 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -15,71 +15,100 @@ 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, + + // DEV only + checkThatStackIsEmpty(): void, + resetStackAfterFatalErrorInDev(): 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; - index--; -} + if (__DEV__) { + fiberStack[index] = null; + } -export function push(cursor: StackCursor, value: T, fiber: Fiber): void { - index++; + index--; + } - valueStack[index] = cursor.current; + function push(cursor: StackCursor, value: T, fiber: Fiber): void { + index++; - if (__DEV__) { - fiberStack[index] = fiber; - } + valueStack[index] = cursor.current; - cursor.current = value; -} + if (__DEV__) { + fiberStack[index] = fiber; + } -export function reset(): void { - while (index > -1) { - valueStack[index] = null; + cursor.current = value; + } + function checkThatStackIsEmpty() { if (__DEV__) { - fiberStack[index] = null; + if (index !== -1) { + warning( + false, + 'Expected an empty stack. Something was not reset properly.', + ); + } } + } - index--; + function resetStackAfterFatalErrorInDev() { + if (__DEV__) { + index = -1; + valueStack.length = 0; + fiberStack.length = 0; + } } + + return { + createCursor, + isEmpty, + pop, + push, + checkThatStackIsEmpty, + resetStackAfterFatalErrorInDev, + }; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index bb3ce84945d2b..6565e4888df1b 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); @@ -132,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 283c7dc4f8f15..628e1c38806b5 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', () => {