Skip to content

Commit

Permalink
Add Batched Mode (#15502)
Browse files Browse the repository at this point in the history
* Add Batched Mode

React has an unfortunate quirk where updates are sometimes synchronous
-- where React starts rendering immediately within the call stack of
`setState` — and sometimes batched, where updates are flushed at the
end of the current event. Any update that originates within the call
stack of the React event system is batched. This encompasses most
updates, since most updates originate from an event handler like
`onClick` or `onChange`. It also includes updates triggered by lifecycle
methods or effects. But there are also updates that originate outside
React's event system, like timer events, network events, and microtasks
(promise resolution handlers). These are not batched, which results in
both worse performance (multiple render passes instead of single one)
and confusing semantics.

Ideally all updates would be batched by default. Unfortunately, it's
easy for components to accidentally rely on this behavior, so changing
it could break existing apps in subtle ways.

One way to move to a batched-by-default model is to opt into Concurrent
Mode (still experimental). But Concurrent Mode introduces additional
semantic changes that apps may not be ready to adopt.

This commit introduces an additional mode called Batched Mode. Batched
Mode enables a batched-by-default model that defers all updates to the
next React event. Once it begins rendering, React will not yield to
the browser until the entire render is finished.

Batched Mode is superset of Strict Mode. It fires all the same warnings.
It also drops the forked Suspense behavior used by Legacy Mode, in favor
of the proper semantics used by Concurrent Mode.

I have not added any public APIs that expose the new mode yet. I'll do
that in subsequent commits.

* Suspense in Batched Mode

Should have same semantics as Concurrent Mode.

* Use RootTag field to configure type of root

There are three types of roots: Legacy, Batched, and Concurrent.

* flushSync should not flush batched work

Treat Sync and Batched expiration times separately. Only Sync updates
are pushed to our internal queue of synchronous callbacks.

Renamed `flushImmediateQueue` to `flushSyncCallbackQueue` for clarity.
  • Loading branch information
acdlite committed May 13, 2019
1 parent fec74f9 commit 862f499
Show file tree
Hide file tree
Showing 24 changed files with 498 additions and 238 deletions.
3 changes: 2 additions & 1 deletion packages/react-art/src/ReactART.js
Expand Up @@ -7,6 +7,7 @@

import React from 'react';
import ReactVersion from 'shared/ReactVersion';
import {LegacyRoot} from 'shared/ReactRootTags';
import {
createContainer,
updateContainer,
Expand Down Expand Up @@ -65,7 +66,7 @@ class Surface extends React.Component {

this._surface = Mode.Surface(+width, +height, this._tagRef);

this._mountNode = createContainer(this._surface);
this._mountNode = createContainer(this._surface, LegacyRoot, false);
updateContainer(this.props.children, this._mountNode, this);
}

Expand Down
16 changes: 6 additions & 10 deletions packages/react-dom/src/client/ReactDOM.js
Expand Up @@ -8,6 +8,7 @@
*/

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 {
Expand Down Expand Up @@ -52,6 +53,7 @@ import {
accumulateTwoPhaseDispatches,
accumulateDirectDispatches,
} from 'events/EventPropagators';
import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
import {has as hasInstance} from 'shared/ReactInstanceMap';
import ReactVersion from 'shared/ReactVersion';
import ReactSharedInternals from 'shared/ReactSharedInternals';
Expand Down Expand Up @@ -361,12 +363,8 @@ ReactWork.prototype._onCommit = function(): void {
}
};

function ReactRoot(
container: DOMContainer,
isConcurrent: boolean,
hydrate: boolean,
) {
const root = createContainer(container, isConcurrent, hydrate);
function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
const root = createContainer(container, tag, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(
Expand Down Expand Up @@ -531,9 +529,7 @@ function legacyCreateRootFromDOMContainer(
);
}
}
// Legacy roots are not async by default.
const isConcurrent = false;
return new ReactRoot(container, isConcurrent, shouldHydrate);
return new ReactRoot(container, LegacyRoot, shouldHydrate);
}

function legacyRenderSubtreeIntoContainer(
Expand Down Expand Up @@ -849,7 +845,7 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
container._reactHasBeenPassedToCreateRootDEV = true;
}
const hydrate = options != null && options.hydrate === true;
return new ReactRoot(container, true, hydrate);
return new ReactRoot(container, ConcurrentRoot, hydrate);
}

if (enableStableConcurrentModeAPIs) {
Expand Down
16 changes: 6 additions & 10 deletions packages/react-dom/src/fire/ReactFire.js
Expand Up @@ -13,6 +13,7 @@
// console.log('Hello from Fire entry point.');

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 {
Expand Down Expand Up @@ -58,6 +59,7 @@ import {
accumulateTwoPhaseDispatches,
accumulateDirectDispatches,
} from 'events/EventPropagators';
import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
import {has as hasInstance} from 'shared/ReactInstanceMap';
import ReactVersion from 'shared/ReactVersion';
import ReactSharedInternals from 'shared/ReactSharedInternals';
Expand Down Expand Up @@ -367,12 +369,8 @@ ReactWork.prototype._onCommit = function(): void {
}
};

function ReactRoot(
container: DOMContainer,
isConcurrent: boolean,
hydrate: boolean,
) {
const root = createContainer(container, isConcurrent, hydrate);
function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
const root = createContainer(container, tag, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(
Expand Down Expand Up @@ -537,9 +535,7 @@ function legacyCreateRootFromDOMContainer(
);
}
}
// Legacy roots are not async by default.
const isConcurrent = false;
return new ReactRoot(container, isConcurrent, shouldHydrate);
return new ReactRoot(container, LegacyRoot, shouldHydrate);
}

function legacyRenderSubtreeIntoContainer(
Expand Down Expand Up @@ -855,7 +851,7 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
container._reactHasBeenPassedToCreateRootDEV = true;
}
const hydrate = options != null && options.hydrate === true;
return new ReactRoot(container, true, hydrate);
return new ReactRoot(container, ConcurrentRoot, hydrate);
}

if (enableStableConcurrentModeAPIs) {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-renderer/src/ReactFabric.js
Expand Up @@ -33,6 +33,7 @@ import ReactNativeComponent from './ReactNativeComponent';
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';

import {LegacyRoot} from 'shared/ReactRootTags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import getComponentName from 'shared/getComponentName';
import warningWithoutStack from 'shared/warningWithoutStack';
Expand Down Expand Up @@ -119,7 +120,7 @@ const ReactFabric: ReactFabricType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = createContainer(containerTag, false, false);
root = createContainer(containerTag, LegacyRoot, false);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-renderer/src/ReactNativeRenderer.js
Expand Up @@ -40,6 +40,7 @@ import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
import {setNativeProps} from './ReactNativeRendererSharedExports';

import {LegacyRoot} from 'shared/ReactRootTags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import getComponentName from 'shared/getComponentName';
import warningWithoutStack from 'shared/warningWithoutStack';
Expand Down Expand Up @@ -125,7 +126,7 @@ const ReactNativeRenderer: ReactNativeType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = createContainer(containerTag, false, false);
root = createContainer(containerTag, LegacyRoot, false);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
Expand Down
173 changes: 119 additions & 54 deletions packages/react-noop-renderer/src/createReactNoop.js
Expand Up @@ -18,6 +18,7 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {RootTag} from 'shared/ReactRootTags';

import * as Scheduler from 'scheduler/unstable_mock';
import {createPortal} from 'shared/ReactPortal';
Expand All @@ -32,6 +33,7 @@ import enqueueTask from 'shared/enqueueTask';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import warningWithoutStack from 'shared/warningWithoutStack';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags';

type EventTargetChildElement = {
type: string,
Expand Down Expand Up @@ -832,77 +834,145 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return textInstance.text;
}

function getChildren(root) {
if (root) {
return root.children;
} else {
return null;
}
}

function getPendingChildren(root) {
if (root) {
return root.pendingChildren;
} else {
return null;
}
}

function getChildrenAsJSX(root) {
const children = childToJSX(getChildren(root), null);
if (children === null) {
return null;
}
if (Array.isArray(children)) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: REACT_FRAGMENT_TYPE,
key: null,
ref: null,
props: {children},
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
return children;
}

function getPendingChildrenAsJSX(root) {
const children = childToJSX(getChildren(root), null);
if (children === null) {
return null;
}
if (Array.isArray(children)) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: REACT_FRAGMENT_TYPE,
key: null,
ref: null,
props: {children},
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
return children;
}

let idCounter = 0;

const ReactNoop = {
_Scheduler: Scheduler,

getChildren(rootID: string = DEFAULT_ROOT_ID) {
const container = rootContainers.get(rootID);
if (container) {
return container.children;
} else {
return null;
}
return getChildren(container);
},

getPendingChildren(rootID: string = DEFAULT_ROOT_ID) {
const container = rootContainers.get(rootID);
if (container) {
return container.pendingChildren;
} else {
return null;
}
return getPendingChildren(container);
},

getOrCreateRootContainer(
rootID: string = DEFAULT_ROOT_ID,
isConcurrent: boolean = false,
) {
getOrCreateRootContainer(rootID: string = DEFAULT_ROOT_ID, tag: RootTag) {
let root = roots.get(rootID);
if (!root) {
const container = {rootID: rootID, pendingChildren: [], children: []};
rootContainers.set(rootID, container);
root = NoopRenderer.createContainer(container, isConcurrent, false);
root = NoopRenderer.createContainer(container, tag, false);
roots.set(rootID, root);
}
return root.current.stateNode.containerInfo;
},

// TODO: Replace ReactNoop.render with createRoot + root.render
createRoot() {
const container = {
rootID: '' + idCounter++,
pendingChildren: [],
children: [],
};
const fiberRoot = NoopRenderer.createContainer(
container,
ConcurrentRoot,
false,
);
return {
_Scheduler: Scheduler,
render(children: ReactNodeList) {
NoopRenderer.updateContainer(children, fiberRoot, null, null);
},
getChildren() {
return getChildren(fiberRoot);
},
getChildrenAsJSX() {
return getChildrenAsJSX(fiberRoot);
},
};
},

createSyncRoot() {
const container = {
rootID: '' + idCounter++,
pendingChildren: [],
children: [],
};
const fiberRoot = NoopRenderer.createContainer(
container,
BatchedRoot,
false,
);
return {
_Scheduler: Scheduler,
render(children: ReactNodeList) {
NoopRenderer.updateContainer(children, fiberRoot, null, null);
},
getChildren() {
return getChildren(container);
},
getChildrenAsJSX() {
return getChildrenAsJSX(container);
},
};
},

getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
const children = childToJSX(ReactNoop.getChildren(rootID), null);
if (children === null) {
return null;
}
if (Array.isArray(children)) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: REACT_FRAGMENT_TYPE,
key: null,
ref: null,
props: {children},
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
return children;
const container = rootContainers.get(rootID);
return getChildrenAsJSX(container);
},

getPendingChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
const children = childToJSX(ReactNoop.getPendingChildren(rootID), null);
if (children === null) {
return null;
}
if (Array.isArray(children)) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: REACT_FRAGMENT_TYPE,
key: null,
ref: null,
props: {children},
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
return children;
const container = rootContainers.get(rootID);
return getPendingChildrenAsJSX(container);
},

createPortal(
Expand All @@ -920,11 +990,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {

renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
const rootID = DEFAULT_ROOT_ID;
const isConcurrent = false;
const container = ReactNoop.getOrCreateRootContainer(
rootID,
isConcurrent,
);
const container = ReactNoop.getOrCreateRootContainer(rootID, LegacyRoot);
const root = roots.get(container.rootID);
NoopRenderer.updateContainer(element, root, null, callback);
},
Expand All @@ -934,10 +1000,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
rootID: string,
callback: ?Function,
) {
const isConcurrent = true;
const container = ReactNoop.getOrCreateRootContainer(
rootID,
isConcurrent,
ConcurrentRoot,
);
const root = roots.get(container.rootID);
NoopRenderer.updateContainer(element, root, null, callback);
Expand Down

0 comments on commit 862f499

Please sign in to comment.