mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #8634 from acdlite/fibersyncmount
[Fiber] Sync mount and unmount
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user