Remove dormant createBatch experiment (#17035)

* Remove dormant createBatch experiment

In a hybrid React app with multiple roots, `createBatch` is used to
coordinate an update to a root with its imperative container.

We've pivoted away from multi-root, hybrid React apps for now to focus
on single root apps.

This PR removes the API from the codebase. It's possible we'll add back
some version of this feature in the future.

* Remove unused export
This commit is contained in:
Andrew Clark
2019-10-07 14:15:15 -07:00
committed by GitHub
parent cd1b167ad4
commit 71d012ecd0
10 changed files with 147 additions and 812 deletions

View File

@@ -43,35 +43,6 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('');
});
it('`root.render` returns a thenable work object', () => {
const root = ReactDOM.unstable_createRoot(container);
const work = root.render('Hi');
let ops = [];
work.then(() => {
ops.push('inside callback: ' + container.textContent);
});
ops.push('before committing: ' + container.textContent);
Scheduler.unstable_flushAll();
ops.push('after committing: ' + container.textContent);
expect(ops).toEqual([
'before committing: ',
// `then` callback should fire during commit phase
'inside callback: Hi',
'after committing: Hi',
]);
});
it('resolves `work.then` callback synchronously if the work already committed', () => {
const root = ReactDOM.unstable_createRoot(container);
const work = root.render('Hi');
Scheduler.unstable_flushAll();
let ops = [];
work.then(() => {
ops.push('inside callback');
});
expect(ops).toEqual(['inside callback']);
});
it('supports hydration', async () => {
const markup = await new Promise(resolve =>
resolve(
@@ -129,200 +100,6 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('abdc');
});
it('can defer a commit by batching it', () => {
const root = ReactDOM.unstable_createRoot(container);
const batch = root.createBatch();
batch.render(<div>Hi</div>);
// Hasn't committed yet
expect(container.textContent).toEqual('');
// Commit
batch.commit();
expect(container.textContent).toEqual('Hi');
});
it('applies setState in componentDidMount synchronously in a batch', done => {
class App extends React.Component {
state = {mounted: false};
componentDidMount() {
this.setState({
mounted: true,
});
}
render() {
return this.state.mounted ? 'Hi' : 'Bye';
}
}
const root = ReactDOM.unstable_createRoot(container);
const batch = root.createBatch();
batch.render(<App />);
Scheduler.unstable_flushAll();
// Hasn't updated yet
expect(container.textContent).toEqual('');
let ops = [];
batch.then(() => {
// Still hasn't updated
ops.push(container.textContent);
// Should synchronously commit
batch.commit();
ops.push(container.textContent);
expect(ops).toEqual(['', 'Hi']);
done();
});
});
it('does not restart a completed batch when committing if there were no intervening updates', () => {
let ops = [];
function Foo(props) {
ops.push('Foo');
return props.children;
}
const root = ReactDOM.unstable_createRoot(container);
const batch = root.createBatch();
batch.render(<Foo>Hi</Foo>);
// Flush all async work.
Scheduler.unstable_flushAll();
// Root should complete without committing.
expect(ops).toEqual(['Foo']);
expect(container.textContent).toEqual('');
ops = [];
// Commit. Shouldn't re-render Foo.
batch.commit();
expect(ops).toEqual([]);
expect(container.textContent).toEqual('Hi');
});
it('can wait for a batch to finish', () => {
const root = ReactDOM.unstable_createRoot(container);
const batch = root.createBatch();
batch.render('Foo');
Scheduler.unstable_flushAll();
// Hasn't updated yet
expect(container.textContent).toEqual('');
let ops = [];
batch.then(() => {
// Still hasn't updated
ops.push(container.textContent);
// Should synchronously commit
batch.commit();
ops.push(container.textContent);
});
expect(ops).toEqual(['', 'Foo']);
});
it('`batch.render` returns a thenable work object', () => {
const root = ReactDOM.unstable_createRoot(container);
const batch = root.createBatch();
const work = batch.render('Hi');
let ops = [];
work.then(() => {
ops.push('inside callback: ' + container.textContent);
});
ops.push('before committing: ' + container.textContent);
batch.commit();
ops.push('after committing: ' + container.textContent);
expect(ops).toEqual([
'before committing: ',
// `then` callback should fire during commit phase
'inside callback: Hi',
'after committing: Hi',
]);
});
it('can commit an empty batch', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(1);
Scheduler.unstable_advanceTime(2000);
// This batch has a later expiration time than the earlier update.
const batch = root.createBatch();
// This should not flush the earlier update.
batch.commit();
expect(container.textContent).toEqual('');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('1');
});
it('two batches created simultaneously are committed separately', () => {
// (In other words, they have distinct expiration times)
const root = ReactDOM.unstable_createRoot(container);
const batch1 = root.createBatch();
batch1.render(1);
const batch2 = root.createBatch();
batch2.render(2);
expect(container.textContent).toEqual('');
batch1.commit();
expect(container.textContent).toEqual('1');
batch2.commit();
expect(container.textContent).toEqual('2');
});
it('commits an earlier batch without committing a later batch', () => {
const root = ReactDOM.unstable_createRoot(container);
const batch1 = root.createBatch();
batch1.render(1);
// This batch has a later expiration time
Scheduler.unstable_advanceTime(2000);
const batch2 = root.createBatch();
batch2.render(2);
expect(container.textContent).toEqual('');
batch1.commit();
expect(container.textContent).toEqual('1');
batch2.commit();
expect(container.textContent).toEqual('2');
});
it('commits a later batch without committing an earlier batch', () => {
const root = ReactDOM.unstable_createRoot(container);
const batch1 = root.createBatch();
batch1.render(1);
// This batch has a later expiration time
Scheduler.unstable_advanceTime(2000);
const batch2 = root.createBatch();
batch2.render(2);
expect(container.textContent).toEqual('');
batch2.commit();
expect(container.textContent).toEqual('2');
batch1.commit();
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('1');
});
it('handles fatal errors triggered by batch.commit()', () => {
const root = ReactDOM.unstable_createRoot(container);
const batch = root.createBatch();
const InvalidType = undefined;
expect(() => batch.render(<InvalidType />)).toWarnDev(
['React.createElement: type is invalid'],
{withoutStack: true},
);
expect(() => batch.commit()).toThrow('Element type is invalid');
});
it('throws a good message on invalid containers', () => {
expect(() => {
ReactDOM.unstable_createRoot(<div>Hi</div>);

View File

@@ -11,19 +11,13 @@ import type {ReactNodeList} from 'shared/ReactTypes';
import type {RootTag} from 'shared/ReactRootTags';
// TODO: This type is shared between the reconciler and ReactDOM, but will
// eventually be lifted out to the renderer.
import type {
FiberRoot,
Batch as FiberRootBatch,
} from 'react-reconciler/src/ReactFiberRoot';
import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
import '../shared/checkReact';
import './ReactDOMClientInjection';
import {
computeUniqueAsyncExpiration,
findHostInstanceWithNoPortals,
updateContainerAtExpirationTime,
flushRoot,
createContainer,
updateContainer,
batchedEventUpdates,
@@ -179,209 +173,21 @@ setRestoreImplementation(restoreControlledState);
export type DOMContainer =
| (Element & {
_reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
_reactRootContainer: ?_ReactRoot,
_reactHasBeenPassedToCreateRootDEV: ?boolean,
})
| (Document & {
_reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
_reactRootContainer: ?_ReactRoot,
_reactHasBeenPassedToCreateRootDEV: ?boolean,
});
type Batch = FiberRootBatch & {
render(children: ReactNodeList): Work,
then(onComplete: () => mixed): void,
commit(): void,
// The ReactRoot constructor is hoisted but the prototype methods are not. If
// we move ReactRoot to be above ReactBatch, the inverse error occurs.
// $FlowFixMe Hoisting issue.
_root: _ReactRoot | _ReactSyncRoot,
_hasChildren: boolean,
_children: ReactNodeList,
_callbacks: Array<() => mixed> | null,
_didComplete: boolean,
};
type _ReactSyncRoot = {
render(children: ReactNodeList, callback: ?() => mixed): Work,
unmount(callback: ?() => mixed): Work,
type _ReactRoot = {
render(children: ReactNodeList, callback: ?() => mixed): void,
unmount(callback: ?() => mixed): void,
_internalRoot: FiberRoot,
};
type _ReactRoot = _ReactSyncRoot & {
createBatch(): Batch,
};
function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
const expirationTime = computeUniqueAsyncExpiration();
this._expirationTime = expirationTime;
this._root = root;
this._next = null;
this._callbacks = null;
this._didComplete = false;
this._hasChildren = false;
this._children = null;
this._defer = true;
}
ReactBatch.prototype.render = function(children: ReactNodeList) {
invariant(
this._defer,
'batch.render: Cannot render a batch that already committed.',
);
this._hasChildren = true;
this._children = children;
const internalRoot = this._root._internalRoot;
const expirationTime = this._expirationTime;
const work = new ReactWork();
updateContainerAtExpirationTime(
children,
internalRoot,
null,
expirationTime,
null,
work._onCommit,
);
return work;
};
ReactBatch.prototype.then = function(onComplete: () => mixed) {
if (this._didComplete) {
onComplete();
return;
}
let callbacks = this._callbacks;
if (callbacks === null) {
callbacks = this._callbacks = [];
}
callbacks.push(onComplete);
};
ReactBatch.prototype.commit = function() {
const internalRoot = this._root._internalRoot;
let firstBatch = internalRoot.firstBatch;
invariant(
this._defer && firstBatch !== null,
'batch.commit: Cannot commit a batch multiple times.',
);
if (!this._hasChildren) {
// This batch is empty. Return.
this._next = null;
this._defer = false;
return;
}
let expirationTime = this._expirationTime;
// Ensure this is the first batch in the list.
if (firstBatch !== this) {
// This batch is not the earliest batch. We need to move it to the front.
// Update its expiration time to be the expiration time of the earliest
// batch, so that we can flush it without flushing the other batches.
if (this._hasChildren) {
expirationTime = this._expirationTime = firstBatch._expirationTime;
// Rendering this batch again ensures its children will be the final state
// when we flush (updates are processed in insertion order: last
// update wins).
// TODO: This forces a restart. Should we print a warning?
this.render(this._children);
}
// Remove the batch from the list.
let previous = null;
let batch = firstBatch;
while (batch !== this) {
previous = batch;
batch = batch._next;
}
invariant(
previous !== null,
'batch.commit: Cannot commit a batch multiple times.',
);
previous._next = batch._next;
// Add it to the front.
this._next = firstBatch;
firstBatch = internalRoot.firstBatch = this;
}
// Synchronously flush all the work up to this batch's expiration time.
this._defer = false;
flushRoot(internalRoot, expirationTime);
// Pop the batch from the list.
const next = this._next;
this._next = null;
firstBatch = internalRoot.firstBatch = next;
// Append the next earliest batch's children to the update queue.
if (firstBatch !== null && firstBatch._hasChildren) {
firstBatch.render(firstBatch._children);
}
};
ReactBatch.prototype._onComplete = function() {
if (this._didComplete) {
return;
}
this._didComplete = true;
const callbacks = this._callbacks;
if (callbacks === null) {
return;
}
// TODO: Error handling.
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i];
callback();
}
};
type Work = {
then(onCommit: () => mixed): void,
_onCommit: () => void,
_callbacks: Array<() => mixed> | null,
_didCommit: boolean,
};
function ReactWork() {
this._callbacks = null;
this._didCommit = false;
// TODO: Avoid need to bind by replacing callbacks in the update queue with
// list of Work objects.
this._onCommit = this._onCommit.bind(this);
}
ReactWork.prototype.then = function(onCommit: () => mixed): void {
if (this._didCommit) {
onCommit();
return;
}
let callbacks = this._callbacks;
if (callbacks === null) {
callbacks = this._callbacks = [];
}
callbacks.push(onCommit);
};
ReactWork.prototype._onCommit = function(): void {
if (this._didCommit) {
return;
}
this._didCommit = true;
const callbacks = this._callbacks;
if (callbacks === null) {
return;
}
// TODO: Error handling.
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i];
invariant(
typeof callback === 'function',
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: %s',
callback,
);
callback();
}
};
function createRootImpl(
container: DOMContainer,
tag: RootTag,
@@ -418,64 +224,24 @@ function ReactRoot(container: DOMContainer, options: void | RootOptions) {
ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
): void {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
if (callback !== null) {
work.then(callback);
}
updateContainer(children, root, null, work._onCommit);
return work;
updateContainer(children, root, null, callback);
};
ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
callback: ?() => mixed,
): Work {
): void {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
if (callback !== null) {
work.then(callback);
}
updateContainer(null, root, null, work._onCommit);
return work;
};
// Sync roots cannot create batches. Only concurrent ones.
ReactRoot.prototype.createBatch = function(): Batch {
const batch = new ReactBatch(this);
const expirationTime = batch._expirationTime;
const internalRoot = this._internalRoot;
const firstBatch = internalRoot.firstBatch;
if (firstBatch === null) {
internalRoot.firstBatch = batch;
batch._next = null;
} else {
// Insert sorted by expiration time then insertion order
let insertAfter = null;
let insertBefore = firstBatch;
while (
insertBefore !== null &&
insertBefore._expirationTime >= expirationTime
) {
insertAfter = insertBefore;
insertBefore = insertBefore._next;
}
batch._next = insertBefore;
if (insertAfter !== null) {
insertAfter._next = batch;
}
}
return batch;
updateContainer(null, root, null, callback);
};
/**
@@ -529,7 +295,7 @@ let warnedAboutHydrateAPI = false;
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): _ReactSyncRoot {
): _ReactRoot {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
@@ -593,7 +359,7 @@ function legacyRenderSubtreeIntoContainer(
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
let root: _ReactSyncRoot = (container._reactRootContainer: any);
let root: _ReactRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount
@@ -899,7 +665,7 @@ function createRoot(
function createSyncRoot(
container: DOMContainer,
options?: RootOptions,
): _ReactSyncRoot {
): _ReactRoot {
const functionName = enableStableConcurrentModeAPIs
? 'createRoot'
: 'unstable_createRoot';

View File

@@ -857,7 +857,7 @@ describe('DOMEventResponderSystem', () => {
function Test({counter}) {
const listener = React.unstable_useResponder(TestResponder, {counter});
Scheduler.unstable_yieldValue('Test');
return (
<button listeners={listener} ref={ref}>
Press me
@@ -866,11 +866,8 @@ describe('DOMEventResponderSystem', () => {
}
let root = ReactDOM.unstable_createRoot(container);
let batch = root.createBatch();
batch.render(<Test counter={0} />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
batch.commit();
root.render(<Test counter={0} />);
expect(Scheduler).toFlushAndYield(['Test']);
// Click the button
dispatchClickEvent(ref.current);
@@ -880,10 +877,9 @@ describe('DOMEventResponderSystem', () => {
log.length = 0;
// Increase counter
batch = root.createBatch();
batch.render(<Test counter={1} />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
root.render(<Test counter={1} />);
// Yield before committing
expect(Scheduler).toFlushAndYieldThrough(['Test']);
// Click the button again
dispatchClickEvent(ref.current);
@@ -893,7 +889,7 @@ describe('DOMEventResponderSystem', () => {
log.length = 0;
// Commit
batch.commit();
expect(Scheduler).toFlushAndYield([]);
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 1}]);
});

View File

@@ -22,7 +22,6 @@ import type {RootTag} from 'shared/ReactRootTags';
import * as Scheduler from 'scheduler/unstable_mock';
import {createPortal} from 'shared/ReactPortal';
import expect from 'expect';
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import enqueueTask from 'shared/enqueueTask';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -1198,31 +1197,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
console.log(...bufferedLog);
},
flushWithoutCommitting(
expectedFlush: Array<mixed>,
rootID: string = DEFAULT_ROOT_ID,
) {
const root: any = roots.get(rootID);
const expiration = NoopRenderer.computeUniqueAsyncExpiration();
const batch = {
_defer: true,
_expirationTime: expiration,
_onComplete: () => {
root.firstBatch = null;
},
_next: null,
};
root.firstBatch = batch;
Scheduler.unstable_flushAllWithoutAsserting();
const actual = Scheduler.unstable_clearYields();
expect(actual).toEqual(expectedFlush);
return (expectedCommit: Array<mixed>) => {
batch._defer = false;
NoopRenderer.flushRoot(root, expiration);
expect(Scheduler.unstable_clearYields()).toEqual(expectedCommit);
};
},
getRoot(rootID: string = DEFAULT_ROOT_ID) {
return roots.get(rootID);
},

View File

@@ -18,7 +18,7 @@ import {
scheduleWork,
flushPassiveEffects,
} from './ReactFiberWorkLoop';
import {updateContainerAtExpirationTime} from './ReactFiberReconciler';
import {updateContainer, syncUpdates} from './ReactFiberReconciler';
import {emptyContextObject} from './ReactFiberContext';
import {Sync} from './ReactFiberExpirationTime';
import {
@@ -258,7 +258,9 @@ export let scheduleRoot: ScheduleRoot = (
return;
}
flushPassiveEffects();
updateContainerAtExpirationTime(element, root, null, Sync, null);
syncUpdates(() => {
updateContainer(element, root, null, null);
});
}
};

View File

@@ -19,7 +19,6 @@ import type {
import {FundamentalComponent} from 'shared/ReactWorkTags';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import type {
SuspenseHydrationCallbacks,
SuspenseState,
@@ -51,7 +50,6 @@ import {
import {createFiberRoot} from './ReactFiberRoot';
import {injectInternals} from './ReactFiberDevToolsHook';
import {
computeUniqueAsyncExpiration,
requestCurrentTime,
computeExpirationForFiber,
scheduleWork,
@@ -138,92 +136,6 @@ function getContextForSubtree(
return parentContext;
}
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
callback: ?Function,
) {
if (__DEV__) {
if (
ReactCurrentFiberPhase === 'render' &&
ReactCurrentFiberCurrent !== null &&
!didWarnAboutNestedUpdates
) {
didWarnAboutNestedUpdates = true;
warningWithoutStack(
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.\n\n' +
'Check the render method of %s.',
getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown',
);
}
}
const update = createUpdate(expirationTime, suspenseConfig);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
warningWithoutStack(
typeof callback === 'function',
'render(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callback,
);
update.callback = callback;
}
enqueueUpdate(current, update);
scheduleWork(current, expirationTime);
return expirationTime;
}
export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
callback: ?Function,
) {
// TODO: If this is a nested container, this won't be the root.
const current = container.current;
if (__DEV__) {
if (ReactFiberInstrumentation.debugTool) {
if (current.alternate === null) {
ReactFiberInstrumentation.debugTool.onMountContainer(container);
} else if (element === null) {
ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
} else {
ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
}
}
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
return scheduleRootUpdate(
current,
element,
expirationTime,
suspenseConfig,
callback,
);
}
function findHostInstance(component: Object): PublicInstance | null {
const fiber = getInstance(component);
if (fiber === undefined) {
@@ -333,19 +245,68 @@ export function updateContainer(
current,
suspenseConfig,
);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
suspenseConfig,
callback,
);
if (__DEV__) {
if (ReactFiberInstrumentation.debugTool) {
if (current.alternate === null) {
ReactFiberInstrumentation.debugTool.onMountContainer(container);
} else if (element === null) {
ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
} else {
ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
}
}
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
if (__DEV__) {
if (
ReactCurrentFiberPhase === 'render' &&
ReactCurrentFiberCurrent !== null &&
!didWarnAboutNestedUpdates
) {
didWarnAboutNestedUpdates = true;
warningWithoutStack(
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.\n\n' +
'Check the render method of %s.',
getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown',
);
}
}
const update = createUpdate(expirationTime, suspenseConfig);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
warningWithoutStack(
typeof callback === 'function',
'render(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callback,
);
update.callback = callback;
}
enqueueUpdate(current, update);
scheduleWork(current, expirationTime);
return expirationTime;
}
export {
flushRoot,
computeUniqueAsyncExpiration,
batchedEventUpdates,
batchedUpdates,
unbatchedUpdates,

View File

@@ -26,14 +26,6 @@ import {
import {unstable_getThreadID} from 'scheduler/tracing';
import {NoPriority} from './SchedulerWithReactIntegration';
// TODO: This should be lifted into the renderer.
export type Batch = {
_defer: boolean,
_expirationTime: ExpirationTime,
_onComplete: () => mixed,
_next: Batch | null,
};
export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
type BaseFiberRootProperties = {|
@@ -63,10 +55,6 @@ type BaseFiberRootProperties = {|
pendingContext: Object | null,
// Determines if we should attempt to hydrate on the initial mount
+hydrate: boolean,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
firstBatch: Batch | null,
// Node returned by Scheduler.scheduleCallback
callbackNode: *,
// Expiration of the callback associated with this root
@@ -125,7 +113,6 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.firstBatch = null;
this.callbackNode = null;
this.callbackPriority = NoPriority;
this.firstPendingTime = NoWork;

View File

@@ -200,14 +200,13 @@ const LegacyUnbatchedContext = /* */ 0b001000;
const RenderContext = /* */ 0b010000;
const CommitContext = /* */ 0b100000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0;
const RootFatalErrored = 1;
const RootErrored = 2;
const RootSuspended = 3;
const RootSuspendedWithDelay = 4;
const RootCompleted = 5;
const RootLocked = 6;
export type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
@@ -719,7 +718,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
const finishedWork: Fiber = ((root.finishedWork =
root.current.alternate): any);
root.finishedExpirationTime = expirationTime;
resolveLocksOnRoot(root, expirationTime);
finishConcurrentRender(
root,
finishedWork,
@@ -976,13 +974,6 @@ function finishConcurrentRender(
commitRoot(root);
break;
}
case RootLocked: {
// This root has a lock that prevents it from committing. Exit. If
// we begin work on the root again, without any intervening updates,
// it will finish without doing additional work.
markRootSuspendedAtTime(root, expirationTime);
break;
}
default: {
invariant(false, 'Unknown root exit status.');
}
@@ -1060,12 +1051,10 @@ function performSyncWorkOnRoot(root) {
);
} else {
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended. The only exception is
// if the root is locked (using the unstable_createBatch API).
// will commit it even if something suspended.
stopFinishedWorkLoopTimer();
root.finishedWork = (root.current.alternate: any);
root.finishedExpirationTime = expirationTime;
resolveLocksOnRoot(root, expirationTime);
finishSyncRender(root, workInProgressRootExitStatus, expirationTime);
}
@@ -1079,25 +1068,15 @@ function performSyncWorkOnRoot(root) {
}
function finishSyncRender(root, exitStatus, expirationTime) {
if (exitStatus === RootLocked) {
// This root has a lock that prevents it from committing. Exit. If we
// begin work on the root again, without any intervening updates, it
// will finish without doing additional work.
markRootSuspendedAtTime(root, expirationTime);
} else {
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
if (__DEV__) {
if (
exitStatus === RootSuspended ||
exitStatus === RootSuspendedWithDelay
) {
flushSuspensePriorityWarningInDEV();
}
if (__DEV__) {
if (exitStatus === RootSuspended || exitStatus === RootSuspendedWithDelay) {
flushSuspensePriorityWarningInDEV();
}
commitRoot(root);
}
commitRoot(root);
}
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -1140,21 +1119,6 @@ export function flushDiscreteUpdates() {
flushPassiveEffects();
}
function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
const firstBatch = root.firstBatch;
if (
firstBatch !== null &&
firstBatch._defer &&
firstBatch._expirationTime >= expirationTime
) {
scheduleCallback(NormalPriority, () => {
firstBatch._onComplete();
return null;
});
workInProgressRootExitStatus = RootLocked;
}
}
export function deferredUpdates<A>(fn: () => A): A {
// TODO: Remove in favor of Scheduler.next
return runWithPriority(NormalPriority, fn);

View File

@@ -97,15 +97,6 @@ function loadModules({
};
}
const mockDevToolsForTest = () => {
jest.mock('react-reconciler/src/ReactFiberDevToolsHook', () => ({
injectInternals: () => {},
onCommitRoot: () => {},
onCommitUnmount: () => {},
isDevToolsPresent: true,
}));
};
describe('Profiler', () => {
describe('works in profiling and non-profiling bundles', () => {
[true, false].forEach(enableSchedulerTracing => {
@@ -1205,71 +1196,6 @@ describe('Profiler', () => {
});
});
it('should handle interleaved async yields and batched commits', () => {
jest.resetModules();
mockDevToolsForTest();
loadModules({useNoopRenderer: true});
const Child = ({duration, id}) => {
Scheduler.unstable_advanceTime(duration);
Scheduler.unstable_yieldValue(`Child:render:${id}`);
return null;
};
class Parent extends React.Component {
componentDidMount() {
Scheduler.unstable_yieldValue(
`Parent:componentDidMount:${this.props.id}`,
);
}
render() {
const {duration, id} = this.props;
return (
<>
<Child duration={duration} id={id} />
<Child duration={duration} id={id} />
</>
);
}
}
Scheduler.unstable_advanceTime(50);
ReactNoop.renderToRootWithID(<Parent duration={3} id="one" />, 'one');
// Process up to the <Parent> component, but yield before committing.
// This ensures that the profiler timer still has paused fibers.
const commitFirstRender = ReactNoop.flushWithoutCommitting(
['Child:render:one', 'Child:render:one'],
'one',
);
expect(ReactNoop.getRoot('one').current.actualDuration).toBe(0);
Scheduler.unstable_advanceTime(100);
// Process some async work, but yield before committing it.
ReactNoop.renderToRootWithID(<Parent duration={7} id="two" />, 'two');
expect(Scheduler).toFlushAndYieldThrough(['Child:render:two']);
Scheduler.unstable_advanceTime(150);
// Commit the previously paused, batched work.
commitFirstRender(['Parent:componentDidMount:one']);
expect(ReactNoop.getRoot('one').current.actualDuration).toBe(6);
expect(ReactNoop.getRoot('two').current.actualDuration).toBe(0);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toFlushAndYield([
'Child:render:two',
'Parent:componentDidMount:two',
]);
expect(ReactNoop.getRoot('two').current.actualDuration).toBe(14);
});
describe('interaction tracing', () => {
let onInteractionScheduledWorkCompleted;
let onInteractionTraced;

View File

@@ -14,7 +14,6 @@ let ReactFeatureFlags;
let ReactDOM;
let SchedulerTracing;
let Scheduler;
let ReactCache;
function loadModules() {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -27,12 +26,9 @@ function loadModules() {
SchedulerTracing = require('scheduler/tracing');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactCache = require('react-cache');
}
describe('ProfilerDOM', () => {
let TextResource;
let resourcePromise;
let onInteractionScheduledWorkCompleted;
let onInteractionTraced;
@@ -51,97 +47,83 @@ describe('ProfilerDOM', () => {
onWorkStarted: () => {},
onWorkStopped: () => {},
});
resourcePromise = null;
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
resourcePromise = new Promise(
SchedulerTracing.unstable_wrap((resolve, reject) => {
setTimeout(
SchedulerTracing.unstable_wrap(() => {
resolve(text);
}),
ms,
);
}),
);
return resourcePromise;
}, ([text, ms]) => text);
});
const AsyncText = ({ms, text}) => {
TextResource.read([text, ms]);
return text;
};
function Text(props) {
Scheduler.unstable_yieldValue(props.text);
return props.text;
}
const Text = ({text}) => text;
it('should correctly trace interactions for async roots', async () => {
let resolve;
let thenable = {
then(res) {
resolve = () => {
thenable = null;
res();
};
},
};
it('should correctly trace interactions for async roots', async done => {
let batch, element, interaction;
function Async() {
if (thenable !== null) {
Scheduler.unstable_yieldValue('Suspend! [Async]');
throw thenable;
}
Scheduler.unstable_yieldValue('Async');
return 'Async';
}
const element = document.createElement('div');
const root = ReactDOM.unstable_createRoot(element);
let interaction;
let wrappedResolve;
SchedulerTracing.unstable_trace('initial_event', performance.now(), () => {
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
interaction = Array.from(interactions)[0];
element = document.createElement('div');
const root = ReactDOM.unstable_createRoot(element);
batch = root.createBatch();
batch.render(
root.render(
<React.Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Text" ms={2000} />
<Async />
</React.Suspense>,
);
batch.then(
SchedulerTracing.unstable_wrap(() => {
batch.commit();
expect(element.textContent).toBe('Loading...');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
jest.runAllTimers();
resourcePromise.then(
SchedulerTracing.unstable_wrap(() => {
jest.runAllTimers();
Scheduler.unstable_flushAll();
expect(element.textContent).toBe('Text');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(
onInteractionScheduledWorkCompleted,
).not.toHaveBeenCalled();
// Evaluate in an unwrapped callback,
// Because trace/wrap won't decrement the count within the wrapped callback.
Promise.resolve().then(() => {
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenCalledTimes(1);
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenLastNotifiedOfInteraction(interaction);
expect(interaction.__count).toBe(0);
done();
});
}),
);
}),
);
Scheduler.unstable_flushAll();
wrappedResolve = SchedulerTracing.unstable_wrap(() => resolve());
});
// Render, suspend, and commit fallback
expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
expect(element.textContent).toEqual('Loading...');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
interaction,
);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
Scheduler.unstable_flushAll();
jest.advanceTimersByTime(500);
// Ping React to try rendering again
wrappedResolve();
// Complete the tree without committing it
expect(Scheduler).toFlushAndYieldThrough(['Async']);
// Still showing the fallback
expect(element.textContent).toEqual('Loading...');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
interaction,
);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
expect(Scheduler).toFlushAndYield([]);
expect(element.textContent).toEqual('Async');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
interaction,
);
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
});
});