Support sharing context objects between concurrent renderers (#12779)

* Support concurrent primary and secondary renderers.

As a workaround to support multiple concurrent renderers, we categorize
some renderers as primary and others as secondary. We only expect
there to be two concurrent renderers at most: React Native (primary) and
Fabric (secondary); React DOM (primary) and React ART (secondary).
Secondary renderers store their context values on separate fields.

* Add back concurrent renderer warning

Only warn for two concurrent primary or two concurrent secondary renderers.

* Change "_secondary" suffix to "2"

#EveryBitCounts
This commit is contained in:
Andrew Clark 2018-05-10 18:34:01 -07:00 committed by GitHub
parent 6565795377
commit b0726e9947
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 144 additions and 22 deletions

View File

@ -478,6 +478,9 @@ const ARTRenderer = ReactFiberReconciler({
now: ReactScheduler.now, now: ReactScheduler.now,
// The ART renderer is secondary to the React DOM renderer.
isPrimaryRenderer: false,
mutation: { mutation: {
appendChild(parentInstance, child) { appendChild(parentInstance, child) {
if (child.parentNode === parentInstance) { if (child.parentNode === parentInstance) {

View File

@ -339,6 +339,60 @@ describe('ReactART', () => {
doClick(instance); doClick(instance);
expect(onClick2).toBeCalled(); expect(onClick2).toBeCalled();
}); });
it('can concurrently render with a "primary" renderer while sharing context', () => {
const CurrentRendererContext = React.createContext(null);
function Yield(props) {
testRenderer.unstable_yield(props.value);
return null;
}
let ops = [];
function LogCurrentRenderer() {
return (
<CurrentRendererContext.Consumer>
{currentRenderer => {
ops.push(currentRenderer);
return null;
}}
</CurrentRendererContext.Consumer>
);
}
// Using test renderer instead of the DOM renderer here because async
// testing APIs for the DOM renderer don't exist.
const testRenderer = renderer.create(
<CurrentRendererContext.Provider value="Test">
<Yield value="A" />
<Yield value="B" />
<LogCurrentRenderer />
<Yield value="C" />
</CurrentRendererContext.Provider>,
{
unstable_isAsync: true,
},
);
testRenderer.unstable_flushThrough(['A']);
ReactDOM.render(
<Surface>
<LogCurrentRenderer />
<CurrentRendererContext.Provider value="ART">
<LogCurrentRenderer />
</CurrentRendererContext.Provider>
</Surface>,
container,
);
expect(ops).toEqual([null, 'ART']);
ops = [];
expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']);
expect(ops).toEqual(['Test']);
});
}); });
describe('ReactARTComponents', () => { describe('ReactARTComponents', () => {

View File

@ -690,6 +690,8 @@ const DOMRenderer = ReactFiberReconciler({
now: ReactScheduler.now, now: ReactScheduler.now,
isPrimaryRenderer: true,
mutation: { mutation: {
commitMount( commitMount(
domElement: Instance, domElement: Instance,

View File

@ -217,6 +217,9 @@ const ReactFabricRenderer = ReactFiberReconciler({
now: ReactNativeFrameScheduling.now, now: ReactNativeFrameScheduling.now,
// The Fabric renderer is secondary to the existing React Native renderer.
isPrimaryRenderer: false,
prepareForCommit(): void { prepareForCommit(): void {
// Noop // Noop
}, },

View File

@ -169,6 +169,8 @@ const NativeRenderer = ReactFiberReconciler({
now: ReactNativeFrameScheduling.now, now: ReactNativeFrameScheduling.now,
isPrimaryRenderer: true,
prepareForCommit(): void { prepareForCommit(): void {
// Noop // Noop
}, },

View File

@ -185,6 +185,8 @@ let SharedHostConfig = {
now(): number { now(): number {
return elapsedTimeInMs; return elapsedTimeInMs;
}, },
isPrimaryRenderer: true,
}; };
const NoopRenderer = ReactFiberReconciler({ const NoopRenderer = ReactFiberReconciler({

View File

@ -104,7 +104,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const {pushHostContext, pushHostContainer} = hostContext; const {pushHostContext, pushHostContainer} = hostContext;
const {pushProvider} = newContext; const {
pushProvider,
getContextCurrentValue,
getContextChangedBits,
} = newContext;
const { const {
markActualRenderTimeStarted, markActualRenderTimeStarted,
@ -1048,8 +1052,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const newProps = workInProgress.pendingProps; const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps; const oldProps = workInProgress.memoizedProps;
const newValue = context._currentValue; const newValue = getContextCurrentValue(context);
const changedBits = context._changedBits; const changedBits = getContextChangedBits(context);
if (hasLegacyContextChanged()) { if (hasLegacyContextChanged()) {
// Normally we can bail out on props equality but if context has changed // Normally we can bail out on props equality but if context has changed

View File

@ -11,14 +11,16 @@ import type {Fiber} from './ReactFiber';
import type {ReactContext} from 'shared/ReactTypes'; import type {ReactContext} from 'shared/ReactTypes';
import type {StackCursor, Stack} from './ReactFiberStack'; import type {StackCursor, Stack} from './ReactFiberStack';
import warning from 'fbjs/lib/warning';
export type NewContext = { export type NewContext = {
pushProvider(providerFiber: Fiber): void, pushProvider(providerFiber: Fiber): void,
popProvider(providerFiber: Fiber): void, popProvider(providerFiber: Fiber): void,
getContextCurrentValue(context: ReactContext<any>): any,
getContextChangedBits(context: ReactContext<any>): number,
}; };
export default function(stack: Stack) { import warning from 'fbjs/lib/warning';
export default function(stack: Stack, isPrimaryRenderer: boolean) {
const {createCursor, push, pop} = stack; const {createCursor, push, pop} = stack;
const providerCursor: StackCursor<Fiber | null> = createCursor(null); const providerCursor: StackCursor<Fiber | null> = createCursor(null);
@ -34,21 +36,38 @@ export default function(stack: Stack) {
function pushProvider(providerFiber: Fiber): void { function pushProvider(providerFiber: Fiber): void {
const context: ReactContext<any> = providerFiber.type._context; const context: ReactContext<any> = providerFiber.type._context;
push(changedBitsCursor, context._changedBits, providerFiber); if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber); push(changedBitsCursor, context._changedBits, providerFiber);
push(providerCursor, providerFiber, providerFiber); push(valueCursor, context._currentValue, providerFiber);
push(providerCursor, providerFiber, providerFiber);
context._currentValue = providerFiber.pendingProps.value; context._currentValue = providerFiber.pendingProps.value;
context._changedBits = providerFiber.stateNode; 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;
}
} else {
push(changedBitsCursor, context._changedBits2, providerFiber);
push(valueCursor, context._currentValue2, providerFiber);
push(providerCursor, providerFiber, providerFiber);
if (__DEV__) { context._currentValue2 = providerFiber.pendingProps.value;
warning( context._changedBits2 = providerFiber.stateNode;
context._currentRenderer === null || if (__DEV__) {
context._currentRenderer === rendererSigil, warning(
'Detected multiple renderers concurrently rendering the ' + context._currentRenderer2 === null ||
'same context provider. This is currently unsupported.', context._currentRenderer2 === rendererSigil,
); 'Detected multiple renderers concurrently rendering the ' +
context._currentRenderer = rendererSigil; 'same context provider. This is currently unsupported.',
);
context._currentRenderer2 = rendererSigil;
}
} }
} }
@ -61,12 +80,27 @@ export default function(stack: Stack) {
pop(changedBitsCursor, providerFiber); pop(changedBitsCursor, providerFiber);
const context: ReactContext<any> = providerFiber.type._context; const context: ReactContext<any> = providerFiber.type._context;
context._currentValue = currentValue; if (isPrimaryRenderer) {
context._changedBits = changedBits; context._currentValue = currentValue;
context._changedBits = changedBits;
} else {
context._currentValue2 = currentValue;
context._changedBits2 = changedBits;
}
}
function getContextCurrentValue(context: ReactContext<any>): any {
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
function getContextChangedBits(context: ReactContext<any>): number {
return isPrimaryRenderer ? context._changedBits : context._changedBits2;
} }
return { return {
pushProvider, pushProvider,
popProvider, popProvider,
getContextCurrentValue,
getContextChangedBits,
}; };
} }

View File

@ -95,6 +95,11 @@ export type HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL> = {
now(): number, now(): number,
// Temporary workaround for scenario where multiple renderers concurrently
// render using the same context objects. E.g. React DOM and React ART on the
// same page. DOM is the primary renderer; ART is the secondary renderer.
isPrimaryRenderer: boolean,
+hydration?: HydrationHostConfig<T, P, I, TI, HI, C, CX, PL>, +hydration?: HydrationHostConfig<T, P, I, TI, HI, C, CX, PL>,
+mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>, +mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>,

View File

@ -181,7 +181,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const stack = ReactFiberStack(); const stack = ReactFiberStack();
const hostContext = ReactFiberHostContext(config, stack); const hostContext = ReactFiberHostContext(config, stack);
const legacyContext = ReactFiberLegacyContext(stack); const legacyContext = ReactFiberLegacyContext(stack);
const newContext = ReactFiberNewContext(stack); const newContext = ReactFiberNewContext(stack, config.isPrimaryRenderer);
const profilerTimer = createProfilerTimer(now); const profilerTimer = createProfilerTimer(now);
const {popHostContext, popHostContainer} = hostContext; const {popHostContext, popHostContainer} = hostContext;
const { const {

View File

@ -226,6 +226,8 @@ const TestRenderer = ReactFiberReconciler({
// Even after the reconciler has initialized and read host config values. // Even after the reconciler has initialized and read host config values.
now: () => nowImplementation(), now: () => nowImplementation(),
isPrimaryRenderer: true,
mutation: { mutation: {
commitUpdate( commitUpdate(
instance: Instance, instance: Instance,

View File

@ -36,7 +36,14 @@ export function createContext<T>(
_calculateChangedBits: calculateChangedBits, _calculateChangedBits: calculateChangedBits,
_defaultValue: defaultValue, _defaultValue: defaultValue,
_currentValue: defaultValue, _currentValue: defaultValue,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue2: defaultValue,
_changedBits: 0, _changedBits: 0,
_changedBits2: 0,
// These are circular // These are circular
Provider: (null: any), Provider: (null: any),
Consumer: (null: any), Consumer: (null: any),
@ -50,6 +57,7 @@ export function createContext<T>(
if (__DEV__) { if (__DEV__) {
context._currentRenderer = null; context._currentRenderer = null;
context._currentRenderer2 = null;
} }
return context; return context;

View File

@ -85,10 +85,13 @@ export type ReactContext<T> = {
_defaultValue: T, _defaultValue: T,
_currentValue: T, _currentValue: T,
_currentValue2: T,
_changedBits: number, _changedBits: number,
_changedBits2: number,
// DEV only // DEV only
_currentRenderer?: Object | null, _currentRenderer?: Object | null,
_currentRenderer2?: Object | null,
}; };
export type ReactPortal = { export type ReactPortal = {