Merge pull request #8634 from acdlite/fibersyncmount

[Fiber] Sync mount and unmount
This commit is contained in:
Andrew Clark
2016-12-29 13:11:57 -08:00
committed by GitHub
13 changed files with 207 additions and 97 deletions
+2
View File
@@ -1207,6 +1207,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js
* can opt-in to deferred/animation scheduling inside componentDidMount/Update
* performs Task work even after time runs out
* does not perform animation work after time runs out
* can force synchronous updates with syncUpdates, even inside batchedUpdates
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* can update child nodes of a host instance
@@ -1576,6 +1577,7 @@ src/renderers/shared/shared/__tests__/ReactUpdates-test.js
* unstable_batchedUpdates should return value from a callback
* unmounts and remounts a root in the same batch
* handles reentrant mounting in synchronous mode
* mounts and unmounts are sync even in a batch
src/renderers/shared/shared/__tests__/refs-destruction-test.js
* should remove refs when destroying the parent
+26 -15
View File
@@ -67,9 +67,10 @@ type HostContextDev = {
};
type HostContextProd = string;
type HostContext = HostContextDev | HostContextProd;
let eventsEnabled : ?boolean = null;
let selectionInformation : ?mixed = null;
type CommitInfo = {
eventsEnabled: boolean,
selectionInformation: mixed,
};
var ELEMENT_NODE_TYPE = 1;
var DOC_NODE_TYPE = 9;
@@ -137,17 +138,18 @@ var DOMRenderer = ReactFiberReconciler({
return getChildNamespace(parentNamespace, type);
},
prepareForCommit() : void {
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
prepareForCommit() : CommitInfo {
const eventsEnabled = ReactBrowserEventEmitter.isEnabled();
ReactBrowserEventEmitter.setEnabled(false);
selectionInformation = ReactInputSelection.getSelectionInformation();
return {
eventsEnabled,
selectionInformation: ReactInputSelection.getSelectionInformation(),
};
},
resetAfterCommit() : void {
ReactInputSelection.restoreSelection(selectionInformation);
selectionInformation = null;
ReactBrowserEventEmitter.setEnabled(eventsEnabled);
eventsEnabled = null;
resetAfterCommit(commitInfo : CommitInfo) : void {
ReactInputSelection.restoreSelection(commitInfo.selectionInformation);
ReactBrowserEventEmitter.setEnabled(commitInfo.eventsEnabled);
},
createInstance(
@@ -322,9 +324,15 @@ function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any,
while (container.lastChild) {
container.removeChild(container.lastChild);
}
root = container._reactRootContainer = DOMRenderer.createContainer(container);
const newRoot = DOMRenderer.createContainer(container);
root = container._reactRootContainer = newRoot;
// Initial mount is always sync, even if we're in a batch.
DOMRenderer.syncUpdates(() => {
DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);
});
} else {
DOMRenderer.updateContainer(children, root, parentComponent, callback);
}
DOMRenderer.updateContainer(children, root, parentComponent, callback);
return DOMRenderer.getPublicRootInstance(root);
}
@@ -346,8 +354,11 @@ var ReactDOM = {
unmountComponentAtNode(container : DOMContainerElement) {
warnAboutUnstableUse();
if (container._reactRootContainer) {
return renderSubtreeIntoContainer(null, null, container, () => {
container._reactRootContainer = null;
// Unmount is always sync, even if we're in a batch.
return DOMRenderer.syncUpdates(() => {
return renderSubtreeIntoContainer(null, null, container, () => {
container._reactRootContainer = null;
});
});
}
},
@@ -16,6 +16,7 @@ describe('ReactPerf', () => {
var ReactDOM;
var ReactPerf;
var ReactTestUtils;
var ReactDOMFeatureFlags;
var emptyFunction;
var App;
@@ -38,6 +39,7 @@ describe('ReactPerf', () => {
ReactDOM = require('ReactDOM');
ReactPerf = require('ReactPerf');
ReactTestUtils = require('ReactTestUtils');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
emptyFunction = require('emptyFunction');
App = class extends React.Component {
@@ -614,7 +616,7 @@ describe('ReactPerf', () => {
}
}
class EvilPortal extends React.Component {
componentWillMount() {
componentDidMount() {
var portalContainer = document.createElement('div');
ReactDOM.render(<Evil />, portalContainer);
}
@@ -645,6 +647,10 @@ describe('ReactPerf', () => {
var container = document.createElement('div');
var thrownErr = new Error('Muhaha!');
if (ReactDOMFeatureFlags.useFiber) {
spyOn(console, 'error');
}
class Evil extends React.Component {
componentWillMount() {
throw thrownErr;
@@ -679,6 +685,15 @@ describe('ReactPerf', () => {
}
ReactDOM.unmountComponentAtNode(container);
ReactPerf.stop();
if (ReactDOMFeatureFlags.useFiber) {
// A sync `render` inside cWM will print a warning. That should be the
// only warning.
expect(console.error.calls.count()).toEqual(1);
expect(console.error.calls.argsFor(0)[0]).toMatch(
/Render methods should be a pure function of props and state/
);
}
});
it('should not print errant warnings if portal throws in componentDidMount()', () => {
@@ -694,7 +709,7 @@ describe('ReactPerf', () => {
}
}
class EvilPortal extends React.Component {
componentWillMount() {
componentDidMount() {
var portalContainer = document.createElement('div');
ReactDOM.render(<Evil />, portalContainer);
}
@@ -66,8 +66,8 @@ if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
}
module.exports = function<T, P, I, TI, C, CX>(
config : HostConfig<T, P, I, TI, C, CX>,
module.exports = function<T, P, I, TI, C, CX, CI>(
config : HostConfig<T, P, I, TI, C, CX, CI>,
hostContext : HostContext<C, CX>,
scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void,
getPriorityContext : () => PriorityLevel,
@@ -33,8 +33,8 @@ var {
ContentReset,
} = require('ReactTypeOfSideEffect');
module.exports = function<T, P, I, TI, C, CX>(
config : HostConfig<T, P, I, TI, C, CX>,
module.exports = function<T, P, I, TI, C, CX, CI>(
config : HostConfig<T, P, I, TI, C, CX, CI>,
hostContext : HostContext<C, CX>,
captureError : (failedFiber : Fiber, error: Error) => ?Fiber
) {
@@ -46,8 +46,8 @@ if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
}
module.exports = function<T, P, I, TI, C, CX>(
config : HostConfig<T, P, I, TI, C, CX>,
module.exports = function<T, P, I, TI, C, CX, CI>(
config : HostConfig<T, P, I, TI, C, CX, CI>,
hostContext : HostContext<C, CX>,
) {
const {
@@ -34,8 +34,8 @@ export type HostContext<C, CX> = {
resetHostContainer() : void,
};
module.exports = function<T, P, I, TI, C, CX>(
config : HostConfig<T, P, I, TI, C, CX>
module.exports = function<T, P, I, TI, C, CX, CI>(
config : HostConfig<T, P, I, TI, C, CX, CI>
) : HostContext<C, CX> {
const {
getChildHostContext,
@@ -43,7 +43,7 @@ export type Deadline = {
type OpaqueNode = Fiber;
export type HostConfig<T, P, I, TI, C, CX> = {
export type HostConfig<T, P, I, TI, C, CX, CI> = {
getRootHostContext(rootContainerInstance : C) : CX,
getChildHostContext(parentHostContext : CX, type : T) : CX,
@@ -69,8 +69,8 @@ export type HostConfig<T, P, I, TI, C, CX> = {
scheduleAnimationCallback(callback : () => void) : void,
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,
prepareForCommit() : void,
resetAfterCommit() : void,
prepareForCommit() : CI,
resetAfterCommit(commitInfo : CI) : void,
useSyncScheduling ?: boolean,
};
@@ -100,7 +100,7 @@ getContextForSubtree._injectFiber(function(fiber : Fiber) {
parentContext;
});
module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C, CX>) : Reconciler<C, I, TI> {
module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, TI, C, CX, CI>) : Reconciler<C, I, TI> {
var {
scheduleUpdate,
+104 -67
View File
@@ -69,11 +69,12 @@ var {
if (__DEV__) {
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
var warning = require('warning');
}
var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C, CX>) {
module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, TI, C, CX, CI>) {
const hostContext = ReactFiberHostContext(config);
const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
const { beginWork, beginFailedWork } = ReactFiberBeginWork(
@@ -107,10 +108,12 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// is thrown during reconciliation.
let priorityContextBeforeReconciliation : PriorityLevel = NoWork;
// Keeps track of whether we're currently in a work loop. Used to batch
// nested updates.
// Keeps track of whether we're currently in a work loop.
let isPerformingWork : boolean = false;
// Keeps track of whether sync updates should be downgraded to task updates.
let shouldDeferSyncUpdates : boolean = false;
// The next work in progress fiber that we're currently working on.
let nextUnitOfWork : ?Fiber = null;
let nextPriorityLevel : PriorityLevel = NoWork;
@@ -345,7 +348,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
firstEffect = finishedWork.firstEffect;
}
prepareForCommit();
const commitInfo = prepareForCommit();
// Commit all the side-effects within a tree. We'll do this in two passes.
// The first pass performs all the host insertions, updates, deletions and
@@ -366,7 +369,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
resetAfterCommit();
resetAfterCommit(commitInfo);
// In the second pass we'll perform all life-cycles and ref callbacks.
// Life-cycles happen as a separate pass so that all placements, updates,
@@ -672,10 +675,31 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
if (isPerformingWork) {
throw new Error('performWork was called recursively.');
}
// If performWork is called recursively, we need to save the previous state
// of the scheduler so it can be restored before the function exits.
// Recursion is only possible when using syncUpdates.
const previousPriorityContext = priorityContext;
const previousPriorityContextBeforeReconciliation = priorityContextBeforeReconciliation;
const previousIsPerformingWork = isPerformingWork;
const previousShouldDeferSyncUpdates = shouldDeferSyncUpdates;
const previousNextEffect = nextEffect;
const previousCommitPhaseBoundaries = commitPhaseBoundaries;
const previousFirstUncaughtError = firstUncaughtError;
const previousFatalError = fatalError;
const previousIsCommitting = isCommitting;
const previousIsUnmounting = isUnmounting;
priorityContext = NoWork;
priorityContextBeforeReconciliation = NoWork;
isPerformingWork = true;
shouldDeferSyncUpdates = true;
nextEffect = null;
commitPhaseBoundaries = null;
firstUncaughtError = null;
fatalError = null;
isCommitting = false;
isUnmounting = false;
const isPerformingDeferredWork = Boolean(deadline);
let deadlineHasExpired = false;
@@ -705,39 +729,40 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
} catch (error) {
// We caught an error during either the begin or complete phases.
const failedWork = nextUnitOfWork;
if (!failedWork) {
throw new Error('Should have nextUnitOfWork.');
}
if (failedWork) {
// Reset the priority context to its value before reconcilation.
priorityContext = priorityContextBeforeReconciliation;
// Reset the priority context to its value before reconcilation.
priorityContext = priorityContextBeforeReconciliation;
// "Capture" the error by finding the nearest boundary. If there is no
// error boundary, the nearest host container acts as one. If
// captureError returns null, the error was intentionally ignored.
const maybeBoundary = captureError(failedWork, error);
if (maybeBoundary) {
const boundary = maybeBoundary;
// "Capture" the error by finding the nearest boundary. If there is no
// error boundary, the nearest host container acts as one. If
// captureError returns null, the error was intentionally ignored.
const maybeBoundary = captureError(failedWork, error);
if (maybeBoundary) {
const boundary = maybeBoundary;
// Complete the boundary as if it rendered null. This will unmount
// the failed tree.
beginFailedWork(boundary.alternate, boundary, priorityLevel);
// Complete the boundary as if it rendered null. This will unmount
// the failed tree.
beginFailedWork(boundary.alternate, boundary, priorityLevel);
// The next unit of work is now the boundary that captured the error.
// Conceptually, we're unwinding the stack. We need to unwind the
// context stack, too, from the failed work to the boundary that
// captured the error.
// TODO: If we set the memoized props in beginWork instead of
// completeWork, rather than unwind the stack, we can just restart
// from the root. Can't do that until then because without memoized
// props, the nodes higher up in the tree will rerender unnecessarily.
if (failedWork) {
// The next unit of work is now the boundary that captured the error.
// Conceptually, we're unwinding the stack. We need to unwind the
// context stack, too, from the failed work to the boundary that
// captured the error.
// TODO: If we set the memoized props in beginWork instead of
// completeWork, rather than unwind the stack, we can just restart
// from the root. Can't do that until then because without memoized
// props, the nodes higher up in the tree will rerender unnecessarily.
unwindContexts(failedWork, boundary);
nextUnitOfWork = completeUnitOfWork(boundary);
}
nextUnitOfWork = completeUnitOfWork(boundary);
// Continue performing work
continue;
} else if (!fatalError) {
// There is no current unit of work. This is a worst-case scenario
// and should only be possible if there's a bug in the renderer, e.g.
// inside resetAfterCommit.
fatalError = error;
}
// Continue performing work
continue;
} finally {
priorityContext = priorityContextBeforeReconciliation;
}
@@ -778,22 +803,23 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
// We're done performing work. Time to clean up.
isPerformingWork = false;
capturedErrors = null;
failedBoundaries = null;
const errorToThrow = fatalError || firstUncaughtError;
// It's now safe to throw errors.
if (fatalError) {
let e = fatalError;
fatalError = null;
firstUncaughtError = null;
throw e;
}
if (firstUncaughtError) {
let e = firstUncaughtError;
firstUncaughtError = null;
throw e;
// We're done performing work. Restore the previous state of the scheduler.
priorityContext = previousPriorityContext;
priorityContextBeforeReconciliation = previousPriorityContextBeforeReconciliation;
isPerformingWork = previousIsPerformingWork;
shouldDeferSyncUpdates = previousShouldDeferSyncUpdates;
nextEffect = previousNextEffect;
commitPhaseBoundaries = previousCommitPhaseBoundaries;
firstUncaughtError = previousFirstUncaughtError;
fatalError = previousFatalError;
isCommitting = previousIsCommitting;
isUnmounting = previousIsUnmounting;
// It's safe to throw any unhandled errors.
if (errorToThrow) {
throw errorToThrow;
}
}
@@ -900,6 +926,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function hasCapturedError(fiber : Fiber) : boolean {
// TODO: capturedErrors should store the boundary instance, to avoid needing
// to check the alternate.
return Boolean(
capturedErrors &&
(capturedErrors.has(fiber) || (fiber.alternate && capturedErrors.has(fiber.alternate)))
@@ -907,11 +935,12 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function isFailedBoundary(fiber : Fiber) : boolean {
const res = Boolean(
// TODO: failedBoundaries should store the boundary instance, to avoid
// needing to check the alternate.
return Boolean(
failedBoundaries &&
(failedBoundaries.has(fiber) || (fiber.alternate && failedBoundaries.has(fiber.alternate)))
);
return res;
}
function commitErrorHandling(effectfulFiber : Fiber) {
@@ -1001,8 +1030,17 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function scheduleUpdate(fiber : Fiber, priorityLevel : PriorityLevel) {
// If we're in a batch, downgrade sync priority to task priority
if (priorityLevel === SynchronousPriority && isPerformingWork) {
// Detect if a synchronous update is made during render (or begin phase).
if (priorityLevel === SynchronousPriority && isPerformingWork && !isCommitting) {
if (__DEV__) {
warning(
false,
'Render methods should be a pure function of props and state; ' +
'triggering nested component updates from render is not allowed. ' +
'If necessary, trigger nested updates in componentDidUpdate.'
);
}
// Downgrade to Task priority to prevent an infinite loop.
priorityLevel = TaskPriority;
}
@@ -1035,15 +1073,11 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// schedule a callback to perform work later.
switch (priorityLevel) {
case SynchronousPriority:
// Perform work immediately
performWork(SynchronousPriority);
return;
case TaskPriority:
// If we're already performing work, Task work will be flushed before
// exiting the current batch. So we can skip it here.
if (!isPerformingWork) {
performWork(TaskPriority);
}
// TODO: If we're not already performing work, schedule a
// deferred callback.
return;
case AnimationPriority:
scheduleAnimationCallback(performAnimationWork);
@@ -1064,7 +1098,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function getPriorityContext() : PriorityLevel {
if (priorityContext === SynchronousPriority && isPerformingWork) {
// If we're in a batch, downgrade sync priority to task priority
if (priorityContext === SynchronousPriority && shouldDeferSyncUpdates) {
return TaskPriority;
}
return priorityContext;
@@ -1085,16 +1120,15 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function batchedUpdates<A, R>(fn : (a: A) => R, a : A) : R {
const previousIsPerformingWork = isPerformingWork;
// Simulate that we're performing work so that sync work is batched
isPerformingWork = true;
const previousShouldDeferSyncUpdates = shouldDeferSyncUpdates;
shouldDeferSyncUpdates = true;
try {
return fn(a);
} finally {
isPerformingWork = previousIsPerformingWork;
// If we're not already performing work, we need to flush any task work
shouldDeferSyncUpdates = previousShouldDeferSyncUpdates;
// If we're not already inside a batch, we need to flush any task work
// that was created by the user-provided function.
if (!isPerformingWork) {
if (!shouldDeferSyncUpdates) {
performWork(TaskPriority);
}
}
@@ -1102,11 +1136,14 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
function syncUpdates<A>(fn : () => A) : A {
const previousPriorityContext = priorityContext;
const previousShouldDeferSyncUpdates = shouldDeferSyncUpdates;
priorityContext = SynchronousPriority;
shouldDeferSyncUpdates = false;
try {
return fn();
} finally {
priorityContext = previousPriorityContext;
shouldDeferSyncUpdates = previousShouldDeferSyncUpdates;
}
}
@@ -340,4 +340,14 @@ describe('ReactIncrementalScheduling', () => {
// animation priority.
expect(ReactNoop.getChildren()).toEqual([span(1)]);
});
it('can force synchronous updates with syncUpdates, even inside batchedUpdates', done => {
ReactNoop.batchedUpdates(() => {
ReactNoop.syncUpdates(() => {
ReactNoop.render(<span />);
expect(ReactNoop.getChildren()).toEqual([span()]);
done();
});
});
});
});
@@ -18,6 +18,7 @@ describe('ReactComponentTreeHook', () => {
var ReactInstanceMap;
var ReactComponentTreeHook;
var ReactComponentTreeTestUtils;
var ReactDOMFeatureFlags;
beforeEach(() => {
jest.resetModules();
@@ -28,6 +29,7 @@ describe('ReactComponentTreeHook', () => {
ReactInstanceMap = require('ReactInstanceMap');
ReactComponentTreeHook = require('ReactComponentTreeHook');
ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
});
function assertTreeMatches(pairs) {
@@ -1841,6 +1843,9 @@ describe('ReactComponentTreeHook', () => {
// https://github.com/facebook/react/issues/7187
var el = document.createElement('div');
var portalEl = document.createElement('div');
if (ReactDOMFeatureFlags.useFiber) {
spyOn(console, 'error');
}
class Foo extends React.Component {
componentWillMount() {
ReactDOM.render(<div />, portalEl);
@@ -1850,6 +1855,14 @@ describe('ReactComponentTreeHook', () => {
}
}
ReactDOM.render(<Foo />, el);
if (ReactDOMFeatureFlags.useFiber) {
// A sync `render` inside cWM will print a warning. That should be the
// only warning.
expect(console.error.calls.count()).toEqual(1);
expect(console.error.calls.argsFor(0)[0]).toMatch(
/Render methods should be a pure function of props and state/
);
}
});
it('is created when calling renderToString during render', () => {
@@ -1266,7 +1266,7 @@ describe('ReactCompositeComponent', () => {
var layer = document.createElement('div');
class Component extends React.Component {
componentWillMount() {
componentDidMount() {
ReactDOM.render(<div />, layer);
}
@@ -1142,4 +1142,26 @@ describe('ReactUpdates', () => {
expect(container.textContent).toBe('goodbye');
expect(mounts).toBe(1);
});
it('mounts and unmounts are sync even in a batch', () => {
var container1 = document.createElement('div');
var container2 = document.createElement('div');
let called = false;
class Foo extends React.Component {
componentDidMount() {
called = true;
ReactDOM.render(<div>Hello</div>, container2);
expect(container2.textContent).toEqual('Hello');
ReactDOM.unmountComponentAtNode(container2);
expect(container2.textContent).toEqual('');
}
render() {
return <div>{this.props.step}</div>;
}
}
ReactDOM.render(<Foo />, container1);
expect(called).toEqual(true);
});
});