Skip to content

Commit

Permalink
Add Batched Mode
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Apr 25, 2019
1 parent 3f058de commit 67e8030
Show file tree
Hide file tree
Showing 15 changed files with 255 additions and 106 deletions.
3 changes: 2 additions & 1 deletion packages/react-dom/src/client/ReactDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,8 @@ function ReactRoot(
isConcurrent: boolean,
hydrate: boolean,
) {
const root = createContainer(container, isConcurrent, hydrate);
const isBatched = false;
const root = createContainer(container, isBatched, isConcurrent, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/fire/ReactFire.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,8 @@ function ReactRoot(
isConcurrent: boolean,
hydrate: boolean,
) {
const root = createContainer(container, isConcurrent, hydrate);
const isBatched = false;
const root = createContainer(container, isBatched, isConcurrent, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-renderer/src/ReactNativeRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,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, false, false, false);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
Expand Down
175 changes: 131 additions & 44 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,77 +832,160 @@ 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,
isBatched: boolean,
isConcurrent: boolean,
) {
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,
isBatched,
isConcurrent,
false,
);
roots.set(rootID, root);
}
return root.current.stateNode.containerInfo;
},

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

createSyncRoot() {
const isBatched = true;
const isConcurrent = false;
const container = {
rootID: '' + idCounter++,
pendingChildren: [],
children: [],
};
const fiberRoot = NoopRenderer.createContainer(
container,
isBatched,
isConcurrent,
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,9 +1003,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {

renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
const rootID = DEFAULT_ROOT_ID;
const isBatched = false;
const isConcurrent = false;
const container = ReactNoop.getOrCreateRootContainer(
rootID,
isBatched,
isConcurrent,
);
const root = roots.get(container.rootID);
Expand All @@ -934,9 +1019,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
rootID: string,
callback: ?Function,
) {
const isBatched = true;
const isConcurrent = true;
const container = ReactNoop.getOrCreateRootContainer(
rootID,
isBatched,
isConcurrent,
);
const root = roots.get(container.rootID);
Expand Down
59 changes: 22 additions & 37 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ import getComponentName from 'shared/getComponentName';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {NoWork} from './ReactFiberExpirationTime';
import {
NoContext,
NoMode,
ConcurrentMode,
ProfileMode,
StrictMode,
BatchedMode,
} from './ReactTypeOfMode';
import {
REACT_FORWARD_REF_TYPE,
Expand Down Expand Up @@ -434,8 +435,18 @@ export function createWorkInProgress(
return workInProgress;
}

export function createHostRootFiber(isConcurrent: boolean): Fiber {
let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext;
export function createHostRootFiber(
isBatched: boolean,
isConcurrent: boolean,
): Fiber {
let mode;
if (isConcurrent) {
mode = ConcurrentMode | BatchedMode | StrictMode;
} else if (isBatched) {
mode = BatchedMode | StrictMode;
} else {
mode = NoMode;
}

if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
Expand Down Expand Up @@ -476,19 +487,13 @@ export function createFiberFromTypeAndProps(
key,
);
case REACT_CONCURRENT_MODE_TYPE:
return createFiberFromMode(
pendingProps,
mode | ConcurrentMode | StrictMode,
expirationTime,
key,
);
fiberTag = Mode;
mode |= ConcurrentMode | BatchedMode | StrictMode;
break;
case REACT_STRICT_MODE_TYPE:
return createFiberFromMode(
pendingProps,
mode | StrictMode,
expirationTime,
key,
);
fiberTag = Mode;
mode |= StrictMode;
break;
case REACT_PROFILER_TYPE:
return createFiberFromProfiler(pendingProps, mode, expirationTime, key);
case REACT_SUSPENSE_TYPE:
Expand Down Expand Up @@ -672,26 +677,6 @@ function createFiberFromProfiler(
return fiber;
}

function createFiberFromMode(
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTime,
key: null | string,
): Fiber {
const fiber = createFiber(Mode, pendingProps, key, mode);

// TODO: The Mode fiber shouldn't have a type. It has a tag.
const type =
(mode & ConcurrentMode) === NoContext
? REACT_STRICT_MODE_TYPE
: REACT_CONCURRENT_MODE_TYPE;
fiber.elementType = type;
fiber.type = type;

fiber.expirationTime = expirationTime;
return fiber;
}

export function createFiberFromSuspense(
pendingProps: any,
mode: TypeOfMode,
Expand Down Expand Up @@ -720,7 +705,7 @@ export function createFiberFromText(
}

export function createFiberFromHostInstanceForDeletion(): Fiber {
const fiber = createFiber(HostComponent, null, null, NoContext);
const fiber = createFiber(HostComponent, null, null, NoMode);
// TODO: These should not need a type.
fiber.elementType = 'DELETED';
fiber.type = 'DELETED';
Expand Down Expand Up @@ -751,7 +736,7 @@ export function assignFiberPropertiesInDEV(
if (target === null) {
// This Fiber's initial properties will always be overwritten.
// We only use a Fiber to ensure the same hidden class so DEV isn't slow.
target = createFiber(IndeterminateComponent, null, null, NoContext);
target = createFiber(IndeterminateComponent, null, null, NoMode);
}

// This is intentionally written as a list of all properties.
Expand Down
8 changes: 4 additions & 4 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import {
} from './ReactFiberExpirationTime';
import {
ConcurrentMode,
NoContext,
NoMode,
ProfileMode,
StrictMode,
} from './ReactTypeOfMode';
Expand Down Expand Up @@ -1493,7 +1493,7 @@ function updateSuspenseComponent(
null,
);

if ((workInProgress.mode & ConcurrentMode) === NoContext) {
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// Outside of concurrent mode, we commit the effects from the
// partially completed, timed-out tree, too.
const progressedState: SuspenseState = workInProgress.memoizedState;
Expand Down Expand Up @@ -1546,7 +1546,7 @@ function updateSuspenseComponent(
NoWork,
);

if ((workInProgress.mode & ConcurrentMode) === NoContext) {
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// Outside of concurrent mode, we commit the effects from the
// partially completed, timed-out tree, too.
const progressedState: SuspenseState = workInProgress.memoizedState;
Expand Down Expand Up @@ -1629,7 +1629,7 @@ function updateSuspenseComponent(
// schedule a placement.
// primaryChildFragment.effectTag |= Placement;

if ((workInProgress.mode & ConcurrentMode) === NoContext) {
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// Outside of concurrent mode, we commit the effects from the
// partially completed, timed-out tree, too.
const progressedState: SuspenseState = workInProgress.memoizedState;
Expand Down
Loading

0 comments on commit 67e8030

Please sign in to comment.