diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html
index d2541eee1e93e..1b5941f09b317 100644
--- a/fixtures/fiber-triangle/index.html
+++ b/fixtures/fiber-triangle/index.html
@@ -76,51 +76,54 @@
Fiber Example
}
}
- function SierpinskiTriangle({ x, y, s, children }) {
- if (s <= targetSize) {
- return (
-
+ class SierpinskiTriangle extends React.Component {
+ shouldComponentUpdate(nextProps) {
+ var o = this.props;
+ var n = nextProps;
+ return !(
+ o.x === n.x &&
+ o.y === n.y &&
+ o.s === n.s &&
+ o.children === n.children
);
- return r;
}
- var newSize = s / 2;
- var slowDown = true;
- if (slowDown) {
- var e = performance.now() + 0.8;
- while (performance.now() < e) {
- // Artificially long execution time.
+ render() {
+ let {x, y, s, children} = this.props;
+ if (s <= targetSize) {
+ return (
+
+ );
+ return r;
+ }
+ var newSize = s / 2;
+ var slowDown = true;
+ if (slowDown) {
+ var e = performance.now() + 0.8;
+ while (performance.now() < e) {
+ // Artificially long execution time.
+ }
}
- }
- s /= 2;
+ s /= 2;
- return [
-
- {children}
- ,
-
- {children}
- ,
-
- {children}
- ,
- ];
+ return [
+
+ {children}
+ ,
+
+ {children}
+ ,
+
+ {children}
+ ,
+ ];
+ }
}
- SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) {
- var o = oldProps;
- var n = newProps;
- return !(
- o.x === n.x &&
- o.y === n.y &&
- o.s === n.s &&
- o.children === n.children
- );
- };
class ExampleApplication extends React.Component {
constructor() {
diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js
index a4e340c2f75e1..ca97c71e8d7e5 100644
--- a/src/renderers/art/ReactARTFiberEntry.js
+++ b/src/renderers/art/ReactARTFiberEntry.js
@@ -532,6 +532,8 @@ const ARTRenderer = ReactFiberReconciler({
);
},
+ now: ReactDOMFrameScheduling.now,
+
useSyncScheduling: true,
});
diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js
index 07a84c242ad28..f415d1ffabe2c 100644
--- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js
+++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js
@@ -21,7 +21,6 @@ var ReactDOMFiberOption = require('ReactDOMFiberOption');
var ReactDOMFiberSelect = require('ReactDOMFiberSelect');
var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea');
var {getCurrentFiberOwnerName} = require('ReactDebugCurrentFiber');
-var {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} = require('HTMLNodeType');
var assertValidProps = require('assertValidProps');
var emptyFunction = require('fbjs/lib/emptyFunction');
@@ -183,24 +182,6 @@ if (__DEV__) {
};
}
-function ensureListeningTo(rootContainerElement, registrationName) {
- var isDocumentOrFragment =
- rootContainerElement.nodeType === DOCUMENT_NODE ||
- rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
- var doc = isDocumentOrFragment
- ? rootContainerElement
- : rootContainerElement.ownerDocument;
- listenTo(registrationName, doc);
-}
-
-function getOwnerDocumentFromRootContainer(
- rootContainerElement: Element | Document,
-): Document {
- return rootContainerElement.nodeType === DOCUMENT_NODE
- ? (rootContainerElement: any)
- : rootContainerElement.ownerDocument;
-}
-
// There are so many media events, it makes sense to just
// maintain a list rather than create a `trapBubbledEvent` for each
var mediaEvents = {
@@ -244,7 +225,7 @@ function trapClickOnNonInteractiveElement(node: HTMLElement) {
function setInitialDOMProperties(
domElement: Element,
- rootContainerElement: Element | Document,
+ ownerDocument: Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
@@ -284,7 +265,7 @@ function setInitialDOMProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey);
+ listenTo(propKey, ownerDocument);
}
} else if (isCustomComponentTag) {
DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp);
@@ -338,14 +319,12 @@ var ReactDOMFiberComponent = {
createElement(
type: *,
props: Object,
- rootContainerElement: Element | Document,
+ ownerDocument: Document,
parentNamespace: string,
): Element {
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
- var ownerDocument: Document = getOwnerDocumentFromRootContainer(
- rootContainerElement,
- );
+
var domElement: Element;
var namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
@@ -408,17 +387,15 @@ var ReactDOMFiberComponent = {
return domElement;
},
- createTextNode(text: string, rootContainerElement: Element | Document): Text {
- return getOwnerDocumentFromRootContainer(
- rootContainerElement,
- ).createTextNode(text);
+ createTextNode(text: string, ownerDocument: Document): Text {
+ return ownerDocument.createTextNode(text);
},
setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
- rootContainerElement: Element | Document,
+ ownerDocument: Document,
): void {
var isCustomComponentTag = isCustomComponent(tag, rawProps);
if (__DEV__) {
@@ -513,7 +490,7 @@ var ReactDOMFiberComponent = {
);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange');
+ listenTo('onChange', ownerDocument);
break;
case 'option':
ReactDOMFiberOption.validateProps(domElement, rawProps);
@@ -529,7 +506,7 @@ var ReactDOMFiberComponent = {
);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange');
+ listenTo('onChange', ownerDocument);
break;
case 'textarea':
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
@@ -541,7 +518,7 @@ var ReactDOMFiberComponent = {
);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange');
+ listenTo('onChange', ownerDocument);
break;
default:
props = rawProps;
@@ -551,7 +528,7 @@ var ReactDOMFiberComponent = {
setInitialDOMProperties(
domElement,
- rootContainerElement,
+ ownerDocument,
props,
isCustomComponentTag,
);
@@ -590,7 +567,7 @@ var ReactDOMFiberComponent = {
tag: string,
lastRawProps: Object,
nextRawProps: Object,
- rootContainerElement: Element | Document,
+ ownerDocument: Document,
): null | Array {
if (__DEV__) {
validatePropertiesInDevelopment(tag, nextRawProps);
@@ -768,7 +745,7 @@ var ReactDOMFiberComponent = {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey);
+ listenTo(propKey, ownerDocument);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
@@ -835,7 +812,7 @@ var ReactDOMFiberComponent = {
tag: string,
rawProps: Object,
parentNamespace: string,
- rootContainerElement: Element | Document,
+ ownerDocument: Document,
): null | Array {
if (__DEV__) {
var suppressHydrationWarning =
@@ -924,7 +901,7 @@ var ReactDOMFiberComponent = {
);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange');
+ listenTo('onChange', ownerDocument);
break;
case 'option':
ReactDOMFiberOption.validateProps(domElement, rawProps);
@@ -938,7 +915,7 @@ var ReactDOMFiberComponent = {
);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange');
+ listenTo('onChange', ownerDocument);
break;
case 'textarea':
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
@@ -949,7 +926,7 @@ var ReactDOMFiberComponent = {
);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange');
+ listenTo('onChange', ownerDocument);
break;
}
@@ -1016,7 +993,7 @@ var ReactDOMFiberComponent = {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey);
+ listenTo(propKey, ownerDocument);
}
} else if (__DEV__) {
// Validate that the properties correspond to their expected values.
diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js
index d0dd466bfb4fb..7f10e5ecf3e98 100644
--- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js
+++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js
@@ -10,6 +10,7 @@
'use strict';
+import type {Work} from 'ReactFiberReconciler';
import type {ReactNodeList} from 'ReactTypes';
require('checkReact');
@@ -90,13 +91,21 @@ ReactControlledComponent.injection.injectFiberControlledHostComponent(
type DOMContainer =
| (Element & {
- _reactRootContainer: ?Object,
+ _reactRootContainer?: Object | null,
})
| (Document & {
- _reactRootContainer: ?Object,
+ _reactRootContainer?: Object | null,
});
-type Container = Element | Document;
+type LazyContainer = {
+ namespace: string,
+ ownerDocument: Document,
+ getContainer: () => Element | DOMContainer,
+ _reactRootContainer?: Object | null,
+};
+
+type Container = DOMContainer | LazyContainer;
+
type Props = {
autoFocus?: boolean,
children?: mixed,
@@ -116,6 +125,37 @@ type HostContext = HostContextDev | HostContextProd;
let eventsEnabled: ?boolean = null;
let selectionInformation: ?mixed = null;
+function isLazyContainer(container: Container): boolean {
+ return typeof (container: any).getContainer === 'function';
+}
+
+function getOwnerDocument(container: Container): Document {
+ let ownerDocument;
+ if (isLazyContainer(container)) {
+ const lazyContainer: LazyContainer = (container: any);
+ ownerDocument = lazyContainer.ownerDocument;
+ } else if (container.nodeType === DOCUMENT_NODE) {
+ ownerDocument = (container: any);
+ } else {
+ ownerDocument = container.ownerDocument;
+ }
+ return ownerDocument;
+}
+
+function ensureDOMContainer(container: Container): DOMContainer {
+ if (!isLazyContainer(container)) {
+ return ((container: any): DOMContainer);
+ }
+ const lazyContainer: LazyContainer = (container: any);
+ const domContainer = lazyContainer.getContainer();
+ invariant(
+ container !== null && container !== undefined,
+ // TODO: Better error message.
+ 'Container should have resolved by now',
+ );
+ return domContainer;
+}
+
/**
* True if the supplied DOM node is a valid node element.
*
@@ -166,23 +206,41 @@ var DOMRenderer = ReactFiberReconciler({
getRootHostContext(rootContainerInstance: Container): HostContext {
let type;
let namespace;
- const nodeType = rootContainerInstance.nodeType;
- switch (nodeType) {
- case DOCUMENT_NODE:
- case DOCUMENT_FRAGMENT_NODE: {
- type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
- let root = (rootContainerInstance: any).documentElement;
- namespace = root ? root.namespaceURI : getChildNamespace(null, '');
- break;
+
+ if (isLazyContainer(rootContainerInstance)) {
+ namespace = ((rootContainerInstance: any): LazyContainer).namespace;
+ if (__DEV__) {
+ return {namespace, ancestorInfo: null};
}
- default: {
- const container: any = nodeType === COMMENT_NODE
- ? rootContainerInstance.parentNode
- : rootContainerInstance;
- const ownNamespace = container.namespaceURI || null;
- type = container.tagName;
- namespace = getChildNamespace(ownNamespace, type);
- break;
+ return namespace;
+ } else {
+ switch ((rootContainerInstance: any).nodeType) {
+ case DOCUMENT_NODE: {
+ type = '#document';
+ const root = (rootContainerInstance: any).documentElement;
+ namespace = root ? root.namespaceURI : getChildNamespace(null, '');
+ break;
+ }
+ case DOCUMENT_FRAGMENT_NODE: {
+ type = '#fragment';
+ const root = (rootContainerInstance: any).documentElement;
+ namespace = root ? root.namespaceURI : getChildNamespace(null, '');
+ break;
+ }
+ case COMMENT_NODE: {
+ const container = (rootContainerInstance: any).parentNode;
+ const ownNamespace = container.namespaceURI || null;
+ type = container.tagName;
+ namespace = getChildNamespace(ownNamespace, type);
+ break;
+ }
+ default: {
+ const container = (rootContainerInstance: any);
+ const ownNamespace = container.namespaceURI || null;
+ type = container.tagName;
+ namespace = getChildNamespace(ownNamespace, type);
+ break;
+ }
}
}
if (__DEV__) {
@@ -259,7 +317,7 @@ var DOMRenderer = ReactFiberReconciler({
const domElement: Instance = createElement(
type,
props,
- rootContainerInstance,
+ getOwnerDocument(rootContainerInstance),
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
@@ -280,7 +338,8 @@ var DOMRenderer = ReactFiberReconciler({
props: Props,
rootContainerInstance: Container,
): boolean {
- setInitialProperties(domElement, type, props, rootContainerInstance);
+ const ownerDocument = getOwnerDocument(rootContainerInstance);
+ setInitialProperties(domElement, type, props, ownerDocument);
return shouldAutoFocusHostComponent(type, props);
},
@@ -308,13 +367,8 @@ var DOMRenderer = ReactFiberReconciler({
validateDOMNesting(null, string, ownAncestorInfo);
}
}
- return diffProperties(
- domElement,
- type,
- oldProps,
- newProps,
- rootContainerInstance,
- );
+ const ownerDocument = getOwnerDocument(rootContainerInstance);
+ return diffProperties(domElement, type, oldProps, newProps, ownerDocument);
},
commitMount(
@@ -374,7 +428,8 @@ var DOMRenderer = ReactFiberReconciler({
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(null, text, hostContextDev.ancestorInfo);
}
- var textNode: TextInstance = createTextNode(text, rootContainerInstance);
+ const ownerDocument = getOwnerDocument(rootContainerInstance);
+ const textNode: TextInstance = createTextNode(text, ownerDocument);
precacheFiberNode(internalInstanceHandle, textNode);
return textNode;
},
@@ -395,10 +450,11 @@ var DOMRenderer = ReactFiberReconciler({
container: Container,
child: Instance | TextInstance,
): void {
- if (container.nodeType === COMMENT_NODE) {
- (container.parentNode: any).insertBefore(child, container);
+ const domContainer = ensureDOMContainer(container);
+ if (domContainer.nodeType === COMMENT_NODE) {
+ (domContainer.parentNode: any).insertBefore(child, container);
} else {
- container.appendChild(child);
+ domContainer.appendChild(child);
}
},
@@ -415,10 +471,11 @@ var DOMRenderer = ReactFiberReconciler({
child: Instance | TextInstance,
beforeChild: Instance | TextInstance,
): void {
- if (container.nodeType === COMMENT_NODE) {
- (container.parentNode: any).insertBefore(child, beforeChild);
+ const domContainer = ensureDOMContainer(container);
+ if (domContainer.nodeType === COMMENT_NODE) {
+ (domContainer.parentNode: any).insertBefore(child, beforeChild);
} else {
- container.insertBefore(child, beforeChild);
+ domContainer.insertBefore(child, beforeChild);
}
},
@@ -430,13 +487,16 @@ var DOMRenderer = ReactFiberReconciler({
container: Container,
child: Instance | TextInstance,
): void {
- if (container.nodeType === COMMENT_NODE) {
- (container.parentNode: any).removeChild(child);
+ const domContainer = ensureDOMContainer(container);
+ if (domContainer.nodeType === COMMENT_NODE) {
+ (domContainer.parentNode: any).removeChild(child);
} else {
- container.removeChild(child);
+ domContainer.removeChild(child);
}
},
+ now: ReactDOMFrameScheduling.now,
+
canHydrateInstance(
instance: Instance | TextInstance,
type: string,
@@ -477,6 +537,7 @@ var DOMRenderer = ReactFiberReconciler({
getFirstHydratableChild(
parentInstance: Container | Instance,
): null | Instance | TextInstance {
+ parentInstance = ensureDOMContainer(parentInstance);
let next = parentInstance.firstChild;
// Skip non-hydratable nodes.
while (
@@ -508,12 +569,13 @@ var DOMRenderer = ReactFiberReconciler({
} else {
parentNamespace = ((hostContext: any): HostContextProd);
}
+ const ownerDocument = getOwnerDocument(rootContainerInstance);
return diffHydratedProperties(
instance,
type,
props,
parentNamespace,
- rootContainerInstance,
+ ownerDocument,
);
},
@@ -532,6 +594,7 @@ var DOMRenderer = ReactFiberReconciler({
text: string,
) {
if (__DEV__) {
+ parentContainer = ensureDOMContainer(parentContainer);
warnForUnmatchedText(textInstance, text);
}
},
@@ -553,6 +616,7 @@ var DOMRenderer = ReactFiberReconciler({
instance: Instance | TextInstance,
) {
if (__DEV__) {
+ parentContainer = ensureDOMContainer(parentContainer);
if (instance.nodeType === 1) {
warnForDeletedHydratableElement(parentContainer, (instance: any));
} else {
@@ -582,6 +646,7 @@ var DOMRenderer = ReactFiberReconciler({
props: Props,
) {
if (__DEV__) {
+ parentContainer = ensureDOMContainer(parentContainer);
warnForInsertedHydratedElement(parentContainer, type, props);
}
},
@@ -591,6 +656,7 @@ var DOMRenderer = ReactFiberReconciler({
text: string,
) {
if (__DEV__) {
+ parentContainer = ensureDOMContainer(parentContainer);
warnForInsertedHydratedText(parentContainer, text);
}
},
@@ -745,7 +811,91 @@ function createPortal(
return ReactPortal.createPortal(children, container, null, key);
}
+type PublicRoot = {
+ render(children: ReactNodeList, callback: ?() => mixed): void,
+ prerender(children: ReactNodeList): Work,
+ unmount(callback: ?() => mixed): void,
+
+ _reactRootContainer: *,
+};
+
+type RootOptions = {
+ hydrate?: boolean,
+};
+
+type LazyRootOptions = {
+ namespace?: string,
+ ownerDocument?: Document,
+};
+
+function PublicRootNode(container: Container, hydrate: boolean) {
+ const root = DOMRenderer.createContainer(container);
+ root.hydrate = hydrate;
+ this._reactRootContainer = root;
+}
+PublicRootNode.prototype.render = function(
+ children: ReactNodeList,
+ callback: ?() => mixed,
+): void {
+ const work = DOMRenderer.updateRoot(children, this._reactRootContainer, null);
+ callback = callback === undefined ? null : callback;
+ work.then(() => {
+ work.commit();
+ if (callback !== null) {
+ (callback: any)();
+ }
+ });
+};
+PublicRootNode.prototype.prerender = function(children: ReactNodeList): Work {
+ return DOMRenderer.updateRoot(children, this._reactRootContainer, null);
+};
+PublicRootNode.prototype.unmount = function(callback) {
+ const work = DOMRenderer.updateRoot(null, this._reactRootContainer, null);
+ callback = callback === undefined ? null : callback;
+ work.then(() => {
+ work.commit();
+ if (callback !== null) {
+ (callback: any)();
+ }
+ });
+};
+
var ReactDOMFiber = {
+ unstable_createRoot(
+ container: DOMContainer,
+ options?: RootOptions,
+ ): PublicRoot {
+ let hydrate = false;
+ if (options != null && options.hydrate !== undefined) {
+ hydrate = options.hydrate;
+ }
+ return new PublicRootNode(container, hydrate);
+ },
+
+ unstable_createLazyRoot(
+ getContainer: () => DOMContainer,
+ options?: LazyRootOptions,
+ ): PublicRoot {
+ // Default to HTML namespace
+ let namespace = DOMNamespaces.html;
+ // Default to global document
+ let ownerDocument = document;
+ if (options != null) {
+ if (options.namespace != null) {
+ namespace = options.namespace;
+ }
+ if (options.ownerDocument != null) {
+ ownerDocument = options.ownerDocument;
+ }
+ }
+ const container = {
+ getContainer,
+ namespace,
+ ownerDocument,
+ };
+ return new PublicRootNode(container, false);
+ },
+
createPortal,
findDOMNode(
diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
index 36f63a3b4f095..ba1aab836ae5b 100644
--- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
+++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
@@ -1142,7 +1142,7 @@ describe('ReactDOMFiber', () => {
expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1);
});
- it('should mount into a document fragment', () => {
+ fit('should mount into a document fragment', () => {
var fragment = document.createDocumentFragment();
ReactDOM.render(foo
, fragment);
expect(container.innerHTML).toBe('');
diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js
new file mode 100644
index 0000000000000..885a868cd68f7
--- /dev/null
+++ b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
+
+let React;
+let ReactDOM;
+let ReactDOMServer;
+let ReactFeatureFlags;
+
+describe('ReactDOMAsyncRoot', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactDOMServer = require('react-dom/server');
+ ReactFeatureFlags = require('ReactFeatureFlags');
+ ReactFeatureFlags.enableAsyncSubtreeAPI = true;
+ });
+
+ if (ReactDOMFeatureFlags.useFiber) {
+ it('works in easy mode', () => {
+ const container = document.createElement('div');
+ const root = ReactDOM.unstable_createRoot(container);
+ root.render(Foo
);
+ expect(container.textContent).toEqual('Foo');
+ root.render(Bar
);
+ expect(container.textContent).toEqual('Bar');
+ root.unmount();
+ expect(container.textContent).toEqual('');
+ });
+
+ it('can defer commit using prerender', () => {
+ const Async = React.unstable_AsyncComponent;
+ const container = document.createElement('div');
+ const root = ReactDOM.unstable_createRoot(container);
+ const work = root.prerender(Foo);
+
+ // Hasn't updated yet
+ expect(container.textContent).toEqual('');
+
+ let ops = [];
+ work.then(() => {
+ // Still hasn't updated
+ ops.push(container.textContent);
+ // Should synchronously commit
+ work.commit();
+ ops.push(container.textContent);
+ });
+ // Flush async work
+ jest.runAllTimers();
+ expect(ops).toEqual(['', 'Foo']);
+ });
+
+ it('resolves `then` callback synchronously if update is sync', () => {
+ const container = document.createElement('div');
+ const root = ReactDOM.unstable_createRoot(container);
+ const work = root.prerender(Hi
);
+
+ let ops = [];
+ work.then(() => {
+ work.commit();
+ ops.push(container.textContent);
+ expect(container.textContent).toEqual('Hi');
+ });
+ // `then` should have synchronously resolved
+ expect(ops).toEqual(['Hi']);
+ });
+
+ it('supports hydration', async () => {
+ const markup = await new Promise(resolve =>
+ resolve(
+ ReactDOMServer.renderToString(
),
+ ),
+ );
+
+ spyOn(console, 'error');
+
+ // Does not hydrate by default
+ const container1 = document.createElement('div');
+ container1.innerHTML = markup;
+ const root1 = ReactDOM.unstable_createRoot(container1);
+ root1.render(
);
+ expect(console.error.calls.count()).toBe(0);
+
+ // Accepts `hydrate` option
+ const container2 = document.createElement('div');
+ container2.innerHTML = markup;
+ const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
+ root2.render(
);
+ expect(console.error.calls.count()).toBe(1);
+ expect(console.error.calls.argsFor(0)[0]).toMatch('Extra attributes');
+ });
+
+ it('supports lazy containers', () => {
+ let ops = [];
+ function Foo(props) {
+ ops.push('Foo');
+ return props.children;
+ }
+
+ let container;
+ const root = ReactDOM.unstable_createLazyRoot(() => container);
+ const work = root.prerender(Hi);
+ expect(ops).toEqual(['Foo']);
+
+ // Set container
+ container = document.createElement('div');
+
+ ops = [];
+
+ work.commit();
+ expect(container.textContent).toEqual('Hi');
+ // Should not have re-rendered Foo
+ expect(ops).toEqual([]);
+ });
+
+ it('can specify namespace of a lazy container', () => {
+ const namespace = 'http://www.w3.org/2000/svg';
+
+ let container;
+ const root = ReactDOM.unstable_createLazyRoot(() => container, {
+ namespace,
+ });
+ const work = root.prerender();
+
+ // Set container
+ container = document.createElementNS(namespace, 'svg');
+ work.commit();
+ // Child should have svg namespace
+ expect(container.firstChild.namespaceURI).toBe(namespace);
+ });
+ } else {
+ it('does not apply to stack');
+ }
+});
diff --git a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js
index 799ee85c1f55e..443c84b1fe831 100644
--- a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js
+++ b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js
@@ -227,6 +227,11 @@ const NativeRTRenderer = ReactFiberReconciler({
},
useSyncScheduling: true,
+
+ now(): number {
+ // TODO: Enable expiration by implementing this method.
+ return 0;
+ },
});
module.exports = NativeRTRenderer;
diff --git a/src/renderers/native/ReactNativeFiberRenderer.js b/src/renderers/native/ReactNativeFiberRenderer.js
index 8f36d4886a51a..859f422f4c2ac 100644
--- a/src/renderers/native/ReactNativeFiberRenderer.js
+++ b/src/renderers/native/ReactNativeFiberRenderer.js
@@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({
},
useSyncScheduling: true,
+
+ now(): number {
+ // TODO: Enable expiration by implementing this method.
+ return 0;
+ },
});
module.exports = NativeRenderer;
diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js
index cc98ecb920d94..72da9035db089 100644
--- a/src/renderers/noop/ReactNoopEntry.js
+++ b/src/renderers/noop/ReactNoopEntry.js
@@ -24,6 +24,7 @@ var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
var ReactFiberReconciler = require('ReactFiberReconciler');
var ReactInstanceMap = require('ReactInstanceMap');
var emptyObject = require('fbjs/lib/emptyObject');
+var invariant = require('fbjs/lib/invariant');
var expect = require('jest-matchers');
@@ -83,6 +84,8 @@ function removeChild(
parentInstance.children.splice(index, 1);
}
+let elapsedTimeInMs = 0;
+
var NoopRenderer = ReactFiberReconciler({
getRootHostContext() {
if (failInBeginPhase) {
@@ -201,6 +204,10 @@ var NoopRenderer = ReactFiberReconciler({
prepareForCommit(): void {},
resetAfterCommit(): void {},
+
+ now(): number {
+ return elapsedTimeInMs;
+ },
});
var rootContainers = new Map();
@@ -277,6 +284,27 @@ var ReactNoop = {
}
},
+ createRoot(rootID: string) {
+ rootID = typeof rootID === 'string' ? rootID : DEFAULT_ROOT_ID;
+ invariant(
+ !roots.has(rootID),
+ 'Root with id %s already exists. Choose a different id.',
+ rootID,
+ );
+ const container = {rootID: rootID, children: []};
+ rootContainers.set(rootID, container);
+ const root = NoopRenderer.createContainer(container);
+ roots.set(rootID, root);
+ return {
+ prerender(children: any) {
+ return NoopRenderer.updateRoot(children, root, null);
+ },
+ getChildren() {
+ return ReactNoop.getChildren(rootID);
+ },
+ };
+ },
+
findInstance(
componentOrElement: Element | ?React$Component,
): null | Instance | TextInstance {
@@ -336,6 +364,14 @@ var ReactNoop = {
expect(actual).toEqual(expected);
},
+ expire(ms: number): void {
+ elapsedTimeInMs += ms;
+ },
+
+ flushExpired(): Array {
+ return ReactNoop.flushUnitsOfWork(0);
+ },
+
yield(value: mixed) {
if (yieldedValues === null) {
yieldedValues = [value];
@@ -389,7 +425,7 @@ var ReactNoop = {
logHostInstances(container.children, depth + 1);
}
- function logUpdateQueue(updateQueue: UpdateQueue, depth) {
+ function logUpdateQueue(updateQueue: UpdateQueue, depth) {
log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
const firstUpdate = updateQueue.first;
if (!firstUpdate) {
@@ -400,7 +436,7 @@ var ReactNoop = {
' '.repeat(depth + 1) + '~',
firstUpdate && firstUpdate.partialState,
firstUpdate.callback ? 'with callback' : '',
- '[' + firstUpdate.priorityLevel + ']',
+ '[' + firstUpdate.expirationTime + ']',
);
var next;
while ((next = firstUpdate.next)) {
@@ -408,7 +444,7 @@ var ReactNoop = {
' '.repeat(depth + 1) + '~',
next.partialState,
next.callback ? 'with callback' : '',
- '[' + firstUpdate.priorityLevel + ']',
+ '[' + firstUpdate.expirationTime + ']',
);
}
}
@@ -418,7 +454,7 @@ var ReactNoop = {
' '.repeat(depth) +
'- ' +
(fiber.type ? fiber.type.name || fiber.type : '[root]'),
- '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']',
+ '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
);
if (fiber.updateQueue) {
logUpdateQueue(fiber.updateQueue, depth);
diff --git a/src/renderers/shared/ReactDOMFrameScheduling.js b/src/renderers/shared/ReactDOMFrameScheduling.js
index de8d0da98f379..5a2fc01ca989a 100644
--- a/src/renderers/shared/ReactDOMFrameScheduling.js
+++ b/src/renderers/shared/ReactDOMFrameScheduling.js
@@ -37,6 +37,20 @@ if (__DEV__) {
}
}
+const hasNativePerformanceNow =
+ typeof performance === 'object' && typeof performance.now === 'function';
+
+let now;
+if (hasNativePerformanceNow) {
+ now = function() {
+ return performance.now();
+ };
+} else {
+ now = function() {
+ return Date.now();
+ };
+}
+
// TODO: There's no way to cancel, because Fiber doesn't atm.
let rIC: (callback: (deadline: Deadline) => void) => number;
@@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) {
var previousFrameTime = 33;
var activeFrameTime = 33;
- var frameDeadlineObject = {
- timeRemaining: typeof performance === 'object' &&
- typeof performance.now === 'function'
- ? function() {
- // We assume that if we have a performance timer that the rAF callback
- // gets a performance timer value. Not sure if this is always true.
- return frameDeadline - performance.now();
- }
- : function() {
- // As a fallback we use Date.now.
- return frameDeadline - Date.now();
- },
- };
+ var frameDeadlineObject;
+ if (hasNativePerformanceNow) {
+ frameDeadlineObject = {
+ timeRemaining() {
+ // We assume that if we have a performance timer that the rAF callback
+ // gets a performance timer value. Not sure if this is always true.
+ return frameDeadline - performance.now();
+ },
+ };
+ } else {
+ frameDeadlineObject = {
+ timeRemaining() {
+ // Fallback to Date.now()
+ return frameDeadline - Date.now();
+ },
+ };
+ }
// We use the postMessage trick to defer idle work until after the repaint.
var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2);
@@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) {
rIC = requestIdleCallback;
}
+exports.now = now;
exports.rIC = rIC;
diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js
index 763ee6d9f3c97..935b897b3ca1b 100644
--- a/src/renderers/shared/fiber/ReactChildFiber.js
+++ b/src/renderers/shared/fiber/ReactChildFiber.js
@@ -13,7 +13,7 @@
import type {ReactElement} from 'ReactElementType';
import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes';
import type {Fiber} from 'ReactFiber';
-import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine');
var {REACT_PORTAL_TYPE} = require('ReactPortal');
@@ -288,19 +288,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
return existingChildren;
}
- function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber {
+ function useFiber(fiber: Fiber, expirationTime: ExpirationTime): Fiber {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
if (shouldClone) {
- const clone = createWorkInProgress(fiber, priority);
+ const clone = createWorkInProgress(fiber, expirationTime);
clone.index = 0;
clone.sibling = null;
return clone;
} else {
- // We override the pending priority even if it is higher, because if
- // we're reconciling at a lower priority that means that this was
+ // We override the expiration time even if it is earlier, because if
+ // we're reconciling at a later time that means that this was
// down-prioritized.
- fiber.pendingWorkPriority = priority;
+ fiber.expirationTime = expirationTime;
fiber.effectTag = NoEffect;
fiber.index = 0;
fiber.sibling = null;
@@ -349,20 +349,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
textContent: string,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(
textContent,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = textContent;
existing.return = returnFiber;
return existing;
@@ -373,21 +373,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (current === null || current.type !== element.type) {
// Insert
const created = createFiberFromElement(
element,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.ref = coerceRef(current, element);
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.ref = coerceRef(current, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
@@ -403,7 +403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
coroutine: ReactCoroutine,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// TODO: Should this also compare handler to determine whether to reuse?
if (current === null || current.tag !== CoroutineComponent) {
@@ -411,13 +411,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
coroutine,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = coroutine;
existing.return = returnFiber;
return existing;
@@ -428,21 +428,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
yieldNode: ReactYield,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (current === null || current.tag !== YieldComponent) {
// Insert
const created = createFiberFromYield(
yieldNode,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.type = yieldNode.value;
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.type = yieldNode.value;
existing.return = returnFiber;
return existing;
@@ -453,7 +453,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (
current === null ||
@@ -465,13 +465,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
portal,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = portal.children || [];
existing.return = returnFiber;
return existing;
@@ -482,20 +482,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
fragment: Iterable<*>,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (current === null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(
fragment,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = fragment;
existing.return = returnFiber;
return existing;
@@ -505,7 +505,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
function createChild(
returnFiber: Fiber,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys. If the previous node is implicitly keyed
@@ -514,7 +514,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromText(
'' + newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -526,7 +526,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromElement(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.ref = coerceRef(null, newChild);
created.return = returnFiber;
@@ -537,7 +537,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -547,7 +547,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromYield(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.type = newChild.value;
created.return = returnFiber;
@@ -558,7 +558,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -569,7 +569,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromFragment(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -591,7 +591,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
@@ -604,14 +604,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (key !== null) {
return null;
}
- return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
+ return updateTextNode(
+ returnFiber,
+ oldFiber,
+ '' + newChild,
+ expirationTime,
+ );
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
- return updateElement(returnFiber, oldFiber, newChild, priority);
+ return updateElement(
+ returnFiber,
+ oldFiber,
+ newChild,
+ expirationTime,
+ );
} else {
return null;
}
@@ -619,7 +629,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
case REACT_COROUTINE_TYPE: {
if (newChild.key === key) {
- return updateCoroutine(returnFiber, oldFiber, newChild, priority);
+ return updateCoroutine(
+ returnFiber,
+ oldFiber,
+ newChild,
+ expirationTime,
+ );
} else {
return null;
}
@@ -630,7 +645,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// we can continue to replace it without aborting even if it is not a
// yield.
if (key === null) {
- return updateYield(returnFiber, oldFiber, newChild, priority);
+ return updateYield(returnFiber, oldFiber, newChild, expirationTime);
} else {
return null;
}
@@ -638,7 +653,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
- return updatePortal(returnFiber, oldFiber, newChild, priority);
+ return updatePortal(
+ returnFiber,
+ oldFiber,
+ newChild,
+ expirationTime,
+ );
} else {
return null;
}
@@ -651,7 +671,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (key !== null) {
return null;
}
- return updateFragment(returnFiber, oldFiber, newChild, priority);
+ return updateFragment(returnFiber, oldFiber, newChild, expirationTime);
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -671,13 +691,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
newIdx: number,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority);
+ return updateTextNode(
+ returnFiber,
+ matchedFiber,
+ '' + newChild,
+ expirationTime,
+ );
}
if (typeof newChild === 'object' && newChild !== null) {
@@ -687,7 +712,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updateElement(returnFiber, matchedFiber, newChild, priority);
+ return updateElement(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
case REACT_COROUTINE_TYPE: {
@@ -695,14 +725,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updateCoroutine(returnFiber, matchedFiber, newChild, priority);
+ return updateCoroutine(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
case REACT_YIELD_TYPE: {
// Yields doesn't have keys, so we neither have to check the old nor
// new node for the key. If both are yields, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateYield(returnFiber, matchedFiber, newChild, priority);
+ return updateYield(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
case REACT_PORTAL_TYPE: {
@@ -710,13 +750,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updatePortal(returnFiber, matchedFiber, newChild, priority);
+ return updatePortal(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateFragment(returnFiber, matchedFiber, newChild, priority);
+ return updateFragment(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -782,7 +832,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// This algorithm can't optimize by searching from boths ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
@@ -830,7 +880,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
oldFiber,
newChildren[newIdx],
- priority,
+ expirationTime,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
@@ -877,7 +927,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
- priority,
+ expirationTime,
);
if (!newFiber) {
continue;
@@ -904,7 +954,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
newIdx,
newChildren[newIdx],
- priority,
+ expirationTime,
);
if (newFiber) {
if (shouldTrackSideEffects) {
@@ -941,7 +991,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
@@ -1005,7 +1055,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
} else {
nextOldFiber = oldFiber.sibling;
}
- const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority);
+ const newFiber = updateSlot(
+ returnFiber,
+ oldFiber,
+ step.value,
+ expirationTime,
+ );
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
@@ -1048,7 +1103,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, (step = newChildren.next())) {
- const newFiber = createChild(returnFiber, step.value, priority);
+ const newFiber = createChild(returnFiber, step.value, expirationTime);
if (newFiber === null) {
continue;
}
@@ -1074,7 +1129,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
newIdx,
step.value,
- priority,
+ expirationTime,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
@@ -1111,7 +1166,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
@@ -1119,7 +1174,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// We already have an existing node so let's just update it and delete
// the rest.
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
- const existing = useFiber(currentFirstChild, priority);
+ const existing = useFiber(currentFirstChild, expirationTime);
existing.pendingProps = textContent;
existing.return = returnFiber;
return existing;
@@ -1130,7 +1185,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromText(
textContent,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -1140,7 +1195,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
@@ -1150,7 +1205,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (child.key === key) {
if (child.type === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.ref = coerceRef(child, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
@@ -1172,7 +1227,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromElement(
element,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.ref = coerceRef(currentFirstChild, element);
created.return = returnFiber;
@@ -1183,7 +1238,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
coroutine: ReactCoroutine,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const key = coroutine.key;
let child = currentFirstChild;
@@ -1193,7 +1248,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (child.key === key) {
if (child.tag === CoroutineComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.pendingProps = coroutine;
existing.return = returnFiber;
return existing;
@@ -1210,7 +1265,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
coroutine,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -1220,14 +1275,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
yieldNode: ReactYield,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// There's no need to check for keys on yields since they're stateless.
let child = currentFirstChild;
if (child !== null) {
if (child.tag === YieldComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.type = yieldNode.value;
existing.return = returnFiber;
return existing;
@@ -1239,7 +1294,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromYield(
yieldNode,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.type = yieldNode.value;
created.return = returnFiber;
@@ -1250,7 +1305,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const key = portal.key;
let child = currentFirstChild;
@@ -1264,7 +1319,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.pendingProps = portal.children || [];
existing.return = returnFiber;
return existing;
@@ -1281,7 +1336,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
portal,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -1294,7 +1349,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
@@ -1304,8 +1359,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
- // Support only the subset of return types that Stack supports. Treat
- // everything else as empty, but log a warning.
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
@@ -1313,7 +1366,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
@@ -1323,17 +1376,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
-
case REACT_YIELD_TYPE:
return placeSingleChild(
reconcileSingleYield(
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
@@ -1343,7 +1395,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
}
@@ -1355,7 +1407,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
'' + newChild,
- priority,
+ expirationTime,
),
);
}
@@ -1365,7 +1417,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
);
}
@@ -1374,7 +1426,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
);
}
@@ -1446,7 +1498,7 @@ exports.cloneChildFibers = function(
let currentChild = workInProgress.child;
let newChild = createWorkInProgress(
currentChild,
- currentChild.pendingWorkPriority,
+ currentChild.expirationTime,
);
// TODO: Pass this as an argument, since it's easy to forget.
newChild.pendingProps = currentChild.pendingProps;
@@ -1457,7 +1509,7 @@ exports.cloneChildFibers = function(
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
- currentChild.pendingWorkPriority,
+ currentChild.expirationTime,
);
newChild.pendingProps = currentChild.pendingProps;
newChild.return = workInProgress;
diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js
index dae77d55cc79b..4edc1d37a09f3 100644
--- a/src/renderers/shared/fiber/ReactFiber.js
+++ b/src/renderers/shared/fiber/ReactFiber.js
@@ -21,6 +21,7 @@ import type {TypeOfWork} from 'ReactTypeOfWork';
import type {TypeOfInternalContext} from 'ReactTypeOfInternalContext';
import type {TypeOfSideEffect} from 'ReactTypeOfSideEffect';
import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
import type {UpdateQueue} from 'ReactFiberUpdateQueue';
var {
@@ -35,7 +36,9 @@ var {
Fragment,
} = require('ReactTypeOfWork');
-var {NoWork} = require('ReactPriorityLevel');
+var {NoWork: NoWorkPriority} = require('ReactPriorityLevel');
+
+var {NoWork} = require('ReactFiberExpirationTime');
var {NoContext} = require('ReactTypeOfInternalContext');
@@ -109,7 +112,7 @@ export type Fiber = {|
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
- updateQueue: UpdateQueue | null,
+ updateQueue: UpdateQueue | null,
// The state used to create the output
memoizedState: any,
@@ -134,8 +137,9 @@ export type Fiber = {|
firstEffect: Fiber | null,
lastEffect: Fiber | null,
- // This will be used to quickly determine if a subtree has no pending changes.
- pendingWorkPriority: PriorityLevel,
+ // Represents a time in the future by which this work should be completed.
+ // This is also used to quickly determine if a subtree has no pending changes.
+ expirationTime: ExpirationTime,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
@@ -189,7 +193,7 @@ function FiberNode(
this.firstEffect = null;
this.lastEffect = null;
- this.pendingWorkPriority = NoWork;
+ this.expirationTime = NoWork;
this.alternate = null;
@@ -233,7 +237,7 @@ function shouldConstruct(Component) {
// This is used to create an alternate fiber to do work on.
exports.createWorkInProgress = function(
current: Fiber,
- renderPriority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
@@ -270,7 +274,7 @@ exports.createWorkInProgress = function(
workInProgress.lastEffect = null;
}
- workInProgress.pendingWorkPriority = renderPriority;
+ workInProgress.expirationTime = expirationTime;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
@@ -296,7 +300,7 @@ exports.createHostRootFiber = function(): Fiber {
exports.createFiberFromElement = function(
element: ReactElement,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
let owner = null;
if (__DEV__) {
@@ -310,7 +314,7 @@ exports.createFiberFromElement = function(
owner,
);
fiber.pendingProps = element.props;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
if (__DEV__) {
fiber._debugSource = element._source;
@@ -323,24 +327,24 @@ exports.createFiberFromElement = function(
exports.createFiberFromFragment = function(
elements: ReactFragment,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// TODO: Consider supporting keyed fragments. Technically, we accidentally
// support that in the existing React.
const fiber = createFiber(Fragment, null, internalContextTag);
fiber.pendingProps = elements;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
return fiber;
};
exports.createFiberFromText = function(
content: string,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(HostText, null, internalContextTag);
fiber.pendingProps = content;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
return fiber;
};
@@ -411,7 +415,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
exports.createFiberFromCoroutine = function(
coroutine: ReactCoroutine,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(
CoroutineComponent,
@@ -420,27 +424,28 @@ exports.createFiberFromCoroutine = function(
);
fiber.type = coroutine.handler;
fiber.pendingProps = coroutine;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
return fiber;
};
exports.createFiberFromYield = function(
yieldNode: ReactYield,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(YieldComponent, null, internalContextTag);
+ fiber.expirationTime = expirationTime;
return fiber;
};
exports.createFiberFromPortal = function(
portal: ReactPortal,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(HostPortal, portal.key, internalContextTag);
fiber.pendingProps = portal.children || [];
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
fiber.stateNode = {
containerInfo: portal.containerInfo,
implementation: portal.implementation,
@@ -452,5 +457,5 @@ exports.largerPriority = function(
p1: PriorityLevel,
p2: PriorityLevel,
): PriorityLevel {
- return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2;
+ return p1 !== NoWorkPriority && (p2 === NoWorkPriority || p2 > p1) ? p1 : p2;
};
diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js
index ed81a1d778944..2d890408b5e0c 100644
--- a/src/renderers/shared/fiber/ReactFiberBeginWork.js
+++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js
@@ -16,7 +16,7 @@ import type {HostContext} from 'ReactFiberHostContext';
import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig} from 'ReactFiberReconciler';
-import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
var {
mountChildFibersInPlace,
@@ -47,7 +47,7 @@ var {
YieldComponent,
Fragment,
} = ReactTypeOfWork;
-var {NoWork, OffscreenPriority} = require('ReactPriorityLevel');
+var {NoWork, Never} = require('ReactFiberExpirationTime');
var {
PerformedWork,
Placement,
@@ -71,8 +71,13 @@ module.exports = function(
config: HostConfig,
hostContext: HostContext,
hydrationContext: HydrationContext,
- scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
- getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
+ scheduleUpdate: (
+ fiber: Fiber,
+ partialState: mixed,
+ callback: (() => mixed) | null,
+ isReplace: boolean,
+ isForced: boolean,
+ ) => void,
) {
const {
shouldSetTextContent,
@@ -94,28 +99,23 @@ module.exports = function(
mountClassInstance,
// resumeMountClassInstance,
updateClassInstance,
- } = ReactFiberClassComponent(
- scheduleUpdate,
- getPriorityContext,
- memoizeProps,
- memoizeState,
- );
+ } = ReactFiberClassComponent(scheduleUpdate, memoizeProps, memoizeState);
+ // TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
function reconcileChildren(current, workInProgress, nextChildren) {
- const priorityLevel = workInProgress.pendingWorkPriority;
- reconcileChildrenAtPriority(
+ reconcileChildrenAtExpirationTime(
current,
workInProgress,
nextChildren,
- priorityLevel,
+ workInProgress.expirationTime,
);
}
- function reconcileChildrenAtPriority(
+ function reconcileChildrenAtExpirationTime(
current,
workInProgress,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
@@ -126,7 +126,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else if (current.child === workInProgress.child) {
// If the current child is the same as the work in progress, it means that
@@ -139,7 +139,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else {
// If, on the other hand, it is already using a clone, that means we've
@@ -149,7 +149,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
}
}
@@ -223,7 +223,7 @@ module.exports = function(
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
) {
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
@@ -235,18 +235,18 @@ module.exports = function(
if (!workInProgress.stateNode) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, workInProgress.pendingProps);
- mountClassInstance(workInProgress, priorityLevel);
+ mountClassInstance(workInProgress, renderExpirationTime);
shouldUpdate = true;
} else {
invariant(false, 'Resuming work not yet implemented.');
// In a resume, we'll already have an instance we can reuse.
- // shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel);
+ // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime);
}
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
- priorityLevel,
+ renderExpirationTime,
);
}
return finishClassComponent(
@@ -318,8 +318,19 @@ module.exports = function(
pushHostContainer(workInProgress, root.containerInfo);
}
- function updateHostRoot(current, workInProgress, priorityLevel) {
+ function updateHostRoot(current, workInProgress, renderExpirationTime) {
+ const root = (workInProgress.stateNode: FiberRoot);
pushHostRootContext(workInProgress);
+ if (root.completedAt === renderExpirationTime) {
+ // The root is already complete. Bail out and commit.
+ // TODO: This is a limited version of resuming that only applies to
+ // the root, to account for the pathological case where a completed
+ // root must be completely restarted before it can commit. Once we
+ // implement resuming for real, this special branch shouldn't
+ // be neccessary.
+ return null;
+ }
+
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
const prevState = workInProgress.memoizedState;
@@ -330,16 +341,17 @@ module.exports = function(
null,
prevState,
null,
- priorityLevel,
+ renderExpirationTime,
);
if (prevState === state) {
// If the state is the same as before, that's a bailout because we had
- // no work matching this priority.
+ // no work that expires at this time.
resetHydrationState();
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
const element = state.element;
if (
+ root.hydrate &&
(current === null || current.child === null) &&
enterHydrationState(workInProgress)
) {
@@ -361,7 +373,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
element,
- priorityLevel,
+ renderExpirationTime,
);
} else {
// Otherwise reset hydration state in case we aborted and resumed another
@@ -377,7 +389,7 @@ module.exports = function(
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
- function updateHostComponent(current, workInProgress, renderPriority) {
+ function updateHostComponent(current, workInProgress, renderExpirationTime) {
pushHostContext(workInProgress);
if (current === null) {
@@ -423,13 +435,13 @@ module.exports = function(
// Check the host config to see if the children are offscreen/hidden.
if (
- renderPriority !== OffscreenPriority &&
+ renderExpirationTime !== Never &&
!useSyncScheduling &&
shouldDeprioritizeSubtree(type, nextProps)
) {
// Down-prioritize the children.
- workInProgress.pendingWorkPriority = OffscreenPriority;
- // Bailout and come back to this fiber later at OffscreenPriority.
+ workInProgress.expirationTime = Never;
+ // Bailout and come back to this fiber later.
return null;
}
@@ -452,7 +464,11 @@ module.exports = function(
return null;
}
- function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
+ function mountIndeterminateComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ ) {
invariant(
current === null,
'An indeterminate component should never have mounted. This error is ' +
@@ -487,7 +503,7 @@ module.exports = function(
// We will invalidate the child context in finishClassComponent() right after rendering.
const hasContext = pushContextProvider(workInProgress);
adoptClassInstance(workInProgress, value);
- mountClassInstance(workInProgress, priorityLevel);
+ mountClassInstance(workInProgress, renderExpirationTime);
return finishClassComponent(current, workInProgress, true, hasContext);
} else {
// Proceed under the assumption that this is a functional component
@@ -532,7 +548,11 @@ module.exports = function(
}
}
- function updateCoroutineComponent(current, workInProgress) {
+ function updateCoroutineComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ ) {
var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine);
if (hasContextChanged()) {
// Normally we can bail out on props equality but if context has changed
@@ -556,30 +576,29 @@ module.exports = function(
}
const nextChildren = nextCoroutine.children;
- const priorityLevel = workInProgress.pendingWorkPriority;
- // The following is a fork of reconcileChildrenAtPriority but using
+ // The following is a fork of reconcileChildrenAtExpirationTime but using
// stateNode to store the child.
if (current === null) {
workInProgress.stateNode = mountChildFibersInPlace(
workInProgress,
workInProgress.stateNode,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else if (current.child === workInProgress.child) {
workInProgress.stateNode = reconcileChildFibers(
workInProgress,
workInProgress.stateNode,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else {
workInProgress.stateNode = reconcileChildFibersInPlace(
workInProgress,
workInProgress.stateNode,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
}
@@ -589,9 +608,12 @@ module.exports = function(
return workInProgress.stateNode;
}
- function updatePortalComponent(current, workInProgress) {
+ function updatePortalComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ ) {
pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
- const priorityLevel = workInProgress.pendingWorkPriority;
let nextChildren = workInProgress.pendingProps;
if (hasContextChanged()) {
// Normally we can bail out on props equality but if context has changed
@@ -621,7 +643,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
memoizeProps(workInProgress, nextChildren);
} else {
@@ -716,11 +738,11 @@ module.exports = function(
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): Fiber | null {
if (
- workInProgress.pendingWorkPriority === NoWork ||
- workInProgress.pendingWorkPriority > priorityLevel
+ workInProgress.expirationTime === NoWork ||
+ workInProgress.expirationTime > renderExpirationTime
) {
return bailoutOnLowPriority(current, workInProgress);
}
@@ -730,16 +752,24 @@ module.exports = function(
return mountIndeterminateComponent(
current,
workInProgress,
- priorityLevel,
+ renderExpirationTime,
);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress);
case ClassComponent:
- return updateClassComponent(current, workInProgress, priorityLevel);
+ return updateClassComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case HostRoot:
- return updateHostRoot(current, workInProgress, priorityLevel);
+ return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
- return updateHostComponent(current, workInProgress, priorityLevel);
+ return updateHostComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case HostText:
return updateHostText(current, workInProgress);
case CoroutineHandlerPhase:
@@ -747,13 +777,21 @@ module.exports = function(
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
- return updateCoroutineComponent(current, workInProgress);
+ return updateCoroutineComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
return null;
case HostPortal:
- return updatePortalComponent(current, workInProgress);
+ return updatePortalComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case Fragment:
return updateFragment(current, workInProgress);
default:
@@ -768,7 +806,7 @@ module.exports = function(
function beginFailedWork(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
) {
// Push context providers here to avoid a push/pop context mismatch.
switch (workInProgress.tag) {
@@ -801,8 +839,8 @@ module.exports = function(
}
if (
- workInProgress.pendingWorkPriority === NoWork ||
- workInProgress.pendingWorkPriority > priorityLevel
+ workInProgress.expirationTime === NoWork ||
+ workInProgress.expirationTime > renderExpirationTime
) {
return bailoutOnLowPriority(current, workInProgress);
}
@@ -814,11 +852,11 @@ module.exports = function(
// Unmount the current children as if the component rendered null
const nextChildren = null;
- reconcileChildrenAtPriority(
+ reconcileChildrenAtExpirationTime(
current,
workInProgress,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
if (workInProgress.tag === ClassComponent) {
diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js
index 2e080e23bff42..6b7cf7f8812d5 100644
--- a/src/renderers/shared/fiber/ReactFiberClassComponent.js
+++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js
@@ -11,7 +11,7 @@
'use strict';
import type {Fiber} from 'ReactFiber';
-import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
var {Update} = require('ReactTypeOfSideEffect');
@@ -24,12 +24,7 @@ var {
getUnmaskedContext,
isContextConsumer,
} = require('ReactFiberContext');
-var {
- addUpdate,
- addReplaceUpdate,
- addForceUpdate,
- beginUpdateQueue,
-} = require('ReactFiberUpdateQueue');
+var {beginUpdateQueue} = require('ReactFiberUpdateQueue');
var {hasContextChanged} = require('ReactFiberContext');
var {isMounted} = require('ReactFiberTreeReflection');
var ReactInstanceMap = require('ReactInstanceMap');
@@ -77,8 +72,13 @@ if (__DEV__) {
}
module.exports = function(
- scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
- getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
+ scheduleUpdate: (
+ fiber: Fiber,
+ partialState: mixed,
+ callback: (() => mixed) | null,
+ isReplace: boolean,
+ isForced: boolean,
+ ) => void,
memoizeProps: (workInProgress: Fiber, props: any) => void,
memoizeState: (workInProgress: Fiber, state: any) => void,
) {
@@ -87,33 +87,27 @@ module.exports = function(
isMounted,
enqueueSetState(instance, partialState, callback) {
const fiber = ReactInstanceMap.get(instance);
- const priorityLevel = getPriorityContext(fiber, false);
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
- addUpdate(fiber, partialState, callback, priorityLevel);
- scheduleUpdate(fiber, priorityLevel);
+ scheduleUpdate(fiber, partialState, callback, false, false);
},
enqueueReplaceState(instance, state, callback) {
const fiber = ReactInstanceMap.get(instance);
- const priorityLevel = getPriorityContext(fiber, false);
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
- addReplaceUpdate(fiber, state, callback, priorityLevel);
- scheduleUpdate(fiber, priorityLevel);
+ scheduleUpdate(fiber, state, callback, true, false);
},
enqueueForceUpdate(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
- const priorityLevel = getPriorityContext(fiber, false);
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
- addForceUpdate(fiber, callback, priorityLevel);
- scheduleUpdate(fiber, priorityLevel);
+ scheduleUpdate(fiber, null, callback, false, true);
},
};
@@ -383,7 +377,7 @@ module.exports = function(
// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): void {
const current = workInProgress.alternate;
@@ -430,7 +424,7 @@ module.exports = function(
instance,
state,
props,
- priorityLevel,
+ renderExpirationTime,
);
}
}
@@ -548,7 +542,7 @@ module.exports = function(
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): boolean {
const instance = workInProgress.stateNode;
resetInputPointers(workInProgress, instance);
@@ -597,7 +591,7 @@ module.exports = function(
instance,
oldState,
newProps,
- priorityLevel,
+ renderExpirationTime,
);
} else {
newState = oldState;
diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js
index 98a8f3def9d1a..de52a071c6caa 100644
--- a/src/renderers/shared/fiber/ReactFiberCommitWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js
@@ -22,7 +22,6 @@ var {
HostPortal,
CoroutineComponent,
} = ReactTypeOfWork;
-var {commitCallbacks} = require('ReactFiberUpdateQueue');
var {onCommitUnmount} = require('ReactFiberDevToolsHook');
var {
invokeGuardedCallback,
@@ -488,6 +487,19 @@ module.exports = function(
}
}
+ function commitCallbacks(callbackList, context) {
+ for (let i = 0; i < callbackList.length; i++) {
+ const callback = callbackList[i];
+ invariant(
+ typeof callback === 'function',
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
+ 'received: %s',
+ callback,
+ );
+ callback.call(context);
+ }
+ }
+
function commitLifeCycles(current: Fiber | null, finishedWork: Fiber): void {
switch (finishedWork.tag) {
case ClassComponent: {
@@ -521,15 +533,27 @@ module.exports = function(
finishedWork.effectTag & Callback &&
finishedWork.updateQueue !== null
) {
- commitCallbacks(finishedWork, finishedWork.updateQueue, instance);
+ const updateQueue = finishedWork.updateQueue;
+ if (updateQueue.callbackList !== null) {
+ // Set the list to null to make sure they don't get called more than once.
+ const callbackList = updateQueue.callbackList;
+ updateQueue.callbackList = null;
+ commitCallbacks(callbackList, instance);
+ }
}
return;
}
case HostRoot: {
const updateQueue = finishedWork.updateQueue;
- if (updateQueue !== null) {
- const instance = finishedWork.child && finishedWork.child.stateNode;
- commitCallbacks(finishedWork, updateQueue, instance);
+ if (updateQueue !== null && updateQueue.callbackList !== null) {
+ // Set the list to null to make sure they don't get called more
+ // than once.
+ const callbackList = updateQueue.callbackList;
+ updateQueue.callbackList = null;
+ const instance = finishedWork.child !== null
+ ? finishedWork.child.stateNode
+ : null;
+ commitCallbacks(callbackList, instance);
}
return;
}
diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
index 451e379965da1..0aaa44f0f579d 100644
--- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
@@ -12,12 +12,13 @@
import type {ReactCoroutine} from 'ReactTypes';
import type {Fiber} from 'ReactFiber';
-import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
import type {HostContext} from 'ReactFiberHostContext';
import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig} from 'ReactFiberReconciler';
+var {topLevelBlockedAt} = require('ReactFiberRoot');
var {reconcileChildFibers} = require('ReactChildFiber');
var {
popContextProvider,
@@ -25,7 +26,7 @@ var {
} = require('ReactFiberContext');
var ReactTypeOfWork = require('ReactTypeOfWork');
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
-var ReactPriorityLevel = require('ReactPriorityLevel');
+var ReactFiberExpirationTime = require('ReactFiberExpirationTime');
var {
IndeterminateComponent,
FunctionalComponent,
@@ -40,7 +41,7 @@ var {
Fragment,
} = ReactTypeOfWork;
var {Placement, Ref, Update} = ReactTypeOfSideEffect;
-var {OffscreenPriority} = ReactPriorityLevel;
+var {NoWork, Never} = ReactFiberExpirationTime;
var invariant = require('fbjs/lib/invariant');
@@ -113,6 +114,7 @@ module.exports = function(
function moveCoroutineToHandlerPhase(
current: Fiber | null,
workInProgress: Fiber,
+ renderExpirationTime: ExpirationTime,
) {
var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine);
invariant(
@@ -139,13 +141,11 @@ module.exports = function(
var nextChildren = fn(props, yields);
var currentFirstChild = current !== null ? current.child : null;
- // Inherit the priority of the returnFiber.
- const priority = workInProgress.pendingWorkPriority;
workInProgress.child = reconcileChildFibers(
workInProgress,
currentFirstChild,
nextChildren,
- priority,
+ renderExpirationTime,
);
return workInProgress.child;
}
@@ -181,15 +181,15 @@ module.exports = function(
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
- renderPriority: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): Fiber | null {
// Get the latest props.
let newProps = workInProgress.pendingProps;
if (newProps === null) {
newProps = workInProgress.memoizedProps;
} else if (
- workInProgress.pendingWorkPriority !== OffscreenPriority ||
- renderPriority === OffscreenPriority
+ workInProgress.expirationTime !== Never ||
+ renderExpirationTime === Never
) {
// Reset the pending props, unless this was a down-prioritization.
workInProgress.pendingProps = null;
@@ -220,6 +220,11 @@ module.exports = function(
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
+
+ // Check if the root is blocked by a top-level update.
+ const blockedAt = topLevelBlockedAt(fiberRoot);
+ fiberRoot.isBlocked =
+ blockedAt !== NoWork && blockedAt <= renderExpirationTime;
return null;
}
case HostComponent: {
@@ -358,7 +363,11 @@ module.exports = function(
return null;
}
case CoroutineComponent:
- return moveCoroutineToHandlerPhase(current, workInProgress);
+ return moveCoroutineToHandlerPhase(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case CoroutineHandlerPhase:
// Reset the tag to now be a first phase coroutine.
workInProgress.tag = CoroutineComponent;
diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js
new file mode 100644
index 0000000000000..c6270858aac5f
--- /dev/null
+++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @providesModule ReactFiberExpirationTime
+ * @flow
+ */
+
+'use strict';
+
+import type {PriorityLevel} from 'ReactPriorityLevel';
+const {
+ NoWork: NoWorkPriority,
+ SynchronousPriority,
+ TaskPriority,
+ HighPriority,
+ LowPriority,
+ OffscreenPriority,
+} = require('ReactPriorityLevel');
+
+const invariant = require('fbjs/lib/invariant');
+
+// TODO: Use an opaque type once ESLint et al support the syntax
+export type ExpirationTime = number;
+
+const NoWork = 0;
+const Sync = 1;
+const Task = 2;
+const Never = Math.pow(2, 31) - 1; // Max int32
+
+const UNIT_SIZE = 10;
+const MAGIC_NUMBER_OFFSET = 10;
+
+exports.NoWork = NoWork;
+exports.Never = Never;
+
+// 1 unit of expiration time represents 10ms.
+function msToExpirationTime(ms: number): ExpirationTime {
+ // Always add an offset so that we don't clash with the magic number for NoWork.
+ return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
+}
+exports.msToExpirationTime = msToExpirationTime;
+
+function ceiling(num: number, precision: number): number {
+ // return Math.ceil(Math.ceil(num * precision) / precision);
+ return (((((num * precision) | 0) + 1) | 0) + 1) / precision;
+}
+
+function bucket(
+ currentTime: ExpirationTime,
+ expirationInMs: number,
+ precisionInMs: number,
+): ExpirationTime {
+ return ceiling(
+ currentTime + expirationInMs / UNIT_SIZE,
+ precisionInMs / UNIT_SIZE,
+ );
+}
+
+// Given the current clock time and a priority level, returns an expiration time
+// that represents a point in the future by which some work should complete.
+// The lower the priority, the further out the expiration time. We use rounding
+// to batch like updates together. The further out the expiration time, the
+// more we want to batch, so we use a larger precision when rounding.
+function priorityToExpirationTime(
+ currentTime: ExpirationTime,
+ priorityLevel: PriorityLevel,
+): ExpirationTime {
+ switch (priorityLevel) {
+ case NoWork:
+ return NoWorkPriority;
+ case SynchronousPriority:
+ return Sync;
+ case TaskPriority:
+ return Task;
+ case HighPriority: {
+ // Should complete within ~100ms. 120ms max.
+ return bucket(currentTime, 100, 20);
+ }
+ case LowPriority: {
+ // Should complete within ~1000ms. 1200ms max.
+ return bucket(currentTime, 1000, 200);
+ }
+ case OffscreenPriority:
+ return Never;
+ default:
+ invariant(
+ false,
+ 'Switch statement should be exhuastive. ' +
+ 'This error is likely caused by a bug in React. Please file an issue.',
+ );
+ }
+}
+exports.priorityToExpirationTime = priorityToExpirationTime;
+
+// Given the current clock time and an expiration time, returns the
+// corresponding priority level. The more time has advanced, the higher the
+// priority level.
+function expirationTimeToPriorityLevel(
+ currentTime: ExpirationTime,
+ expirationTime: ExpirationTime,
+): PriorityLevel {
+ // First check for magic values
+ switch (expirationTime) {
+ case NoWorkPriority:
+ return NoWork;
+ case Sync:
+ return SynchronousPriority;
+ case Task:
+ return TaskPriority;
+ case Never:
+ return OffscreenPriority;
+ default:
+ break;
+ }
+ if (expirationTime <= currentTime) {
+ return TaskPriority;
+ }
+ // TODO: We don't currently distinguish between high and low priority.
+ return LowPriority;
+}
+exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel;
diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js
index 4876d88f4607b..e19b7db48bc9c 100644
--- a/src/renderers/shared/fiber/ReactFiberReconciler.js
+++ b/src/renderers/shared/fiber/ReactFiberReconciler.js
@@ -12,11 +12,17 @@
import type {Fiber} from 'ReactFiber';
import type {FiberRoot} from 'ReactFiberRoot';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
import type {ReactNodeList} from 'ReactTypes';
var ReactFeatureFlags = require('ReactFeatureFlags');
-var {addTopLevelUpdate} = require('ReactFiberUpdateQueue');
+var {
+ insertUpdateIntoFiber,
+ insertUpdateIntoQueue,
+ createUpdateQueue,
+ processUpdateQueue,
+} = require('ReactFiberUpdateQueue');
var {
findCurrentUnmaskedContext,
@@ -49,6 +55,17 @@ export type Deadline = {
type OpaqueHandle = Fiber;
type OpaqueRoot = FiberRoot;
+type Awaitable = {
+ then(resolve: (result: T) => mixed): void,
+};
+
+export type Work = Awaitable & {
+ commit(): void,
+
+ _reactRootContainer: *,
+ _expirationTime: ExpirationTime,
+};
+
export type HostConfig = {
getRootHostContext(rootContainerInstance: C): CX,
getChildHostContext(parentHostContext: CX, type: T, instance: C): CX,
@@ -122,6 +139,8 @@ export type HostConfig = {
prepareForCommit(): void,
resetAfterCommit(): void,
+ now(): number,
+
// Optional hydration
canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean,
canHydrateTextInstance?: (instance: I | TI, text: string) => boolean,
@@ -190,6 +209,11 @@ export type HostConfig = {
export type Reconciler = {
createContainer(containerInfo: C): OpaqueRoot,
+ updateRoot(
+ element: ReactNodeList,
+ container: OpaqueRoot,
+ parentComponent: ?React$Component,
+ ): Work,
updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
@@ -233,8 +257,12 @@ module.exports = function(
var {getPublicInstance} = config;
var {
- scheduleUpdate,
+ scheduleWork,
+ scheduleCompletionCallback,
getPriorityContext,
+ getExpirationTimeForPriority,
+ recalculateCurrentTime,
+ expireWork,
batchedUpdates,
unbatchedUpdates,
flushSync,
@@ -242,10 +270,12 @@ module.exports = function(
} = ReactFiberScheduler(config);
function scheduleTopLevelUpdate(
- current: Fiber,
+ root: FiberRoot,
element: ReactNodeList,
+ currentTime: ExpirationTime,
+ isPrerender: boolean,
callback: ?Function,
- ) {
+ ): ExpirationTime {
if (__DEV__) {
if (
ReactDebugCurrentFiber.phase === 'render' &&
@@ -264,6 +294,8 @@ module.exports = function(
}
}
+ const current = root.current;
+
// Check if the top-level element is an async wrapper component. If so, treat
// updates to the root as async. This is a bit weird but lets us avoid a separate
// `renderAsync` API.
@@ -274,6 +306,10 @@ module.exports = function(
element.type.prototype != null &&
(element.type.prototype: any).unstable_isAsyncReactComponent === true;
const priorityLevel = getPriorityContext(current, forceAsync);
+ const expirationTime = getExpirationTimeForPriority(
+ currentTime,
+ priorityLevel,
+ );
const nextState = {element};
callback = callback === undefined ? null : callback;
if (__DEV__) {
@@ -284,15 +320,129 @@ module.exports = function(
callback,
);
}
- addTopLevelUpdate(current, nextState, callback, priorityLevel);
- scheduleUpdate(current, priorityLevel);
+ const isTopLevelUnmount = nextState.element === null;
+ const update = {
+ priorityLevel,
+ expirationTime,
+ partialState: nextState,
+ callback,
+ isReplace: false,
+ isForced: false,
+ isTopLevelUnmount,
+ next: null,
+ };
+ const update2 = insertUpdateIntoFiber(current, update, currentTime);
+
+ if (isTopLevelUnmount) {
+ // TODO: Redesign the top-level mount/update/unmount API to avoid this
+ // special case.
+ const queue1 = current.updateQueue;
+ const queue2 = current.alternate !== null
+ ? current.alternate.updateQueue
+ : null;
+
+ // Drop all updates that are lower-priority, so that the tree is not
+ // remounted. We need to do this for both queues.
+ if (queue1 !== null && update.next !== null) {
+ update.next = null;
+ queue1.last = update;
+ }
+ if (queue2 !== null && update2 !== null && update2.next !== null) {
+ update2.next = null;
+ queue2.last = update;
+ }
+ }
+
+ if (isPrerender) {
+ // Block the root from committing at this expiration time.
+ if (root.topLevelBlockers === null) {
+ root.topLevelBlockers = createUpdateQueue();
+ }
+ const block = {
+ priorityLevel: null,
+ expirationTime,
+ partialState: null,
+ callback: null,
+ isReplace: false,
+ isForced: false,
+ isTopLevelUnmount: false,
+ next: null,
+ };
+ insertUpdateIntoQueue(root.topLevelBlockers, block, currentTime);
+ }
+
+ scheduleWork(current, expirationTime);
+ return expirationTime;
}
+ function WorkNode(root: OpaqueRoot, expirationTime: ExpirationTime) {
+ this._reactRootContainer = root;
+ this._expirationTime = expirationTime;
+ }
+ WorkNode.prototype.commit = function() {
+ const root = this._reactRootContainer;
+ const expirationTime = this._expirationTime;
+ const topLevelBlockers = root.topLevelBlockers;
+ if (topLevelBlockers === null) {
+ return;
+ }
+ processUpdateQueue(topLevelBlockers, null, null, null, expirationTime);
+ expireWork(root, expirationTime);
+ };
+ WorkNode.prototype.then = function(callback) {
+ const root = this._reactRootContainer;
+ const expirationTime = this._expirationTime;
+ scheduleCompletionCallback(root, callback, expirationTime);
+ };
+
return {
createContainer(containerInfo: C): OpaqueRoot {
return createFiberRoot(containerInfo);
},
+ updateRoot(
+ element: ReactNodeList,
+ container: OpaqueRoot,
+ parentComponent: ?React$Component,
+ ): Work {
+ 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;
+ }
+
+ const currentTime = recalculateCurrentTime();
+ const expirationTime = scheduleTopLevelUpdate(
+ container,
+ element,
+ currentTime,
+ true,
+ null,
+ );
+
+ let completionCallbacks = container.completionCallbacks;
+ if (completionCallbacks === null) {
+ completionCallbacks = createUpdateQueue();
+ }
+
+ return new WorkNode(container, expirationTime);
+ },
+
updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
@@ -321,7 +471,8 @@ module.exports = function(
container.pendingContext = context;
}
- scheduleTopLevelUpdate(current, element, callback);
+ const currentTime = recalculateCurrentTime();
+ scheduleTopLevelUpdate(container, element, currentTime, false, callback);
},
batchedUpdates,
diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js
index f78205d2bcae5..775f3b0919dc0 100644
--- a/src/renderers/shared/fiber/ReactFiberRoot.js
+++ b/src/renderers/shared/fiber/ReactFiberRoot.js
@@ -11,8 +11,12 @@
'use strict';
import type {Fiber} from 'ReactFiber';
+import type {UpdateQueue} from 'ReactFiberUpdateQueue';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
const {createHostRootFiber} = require('ReactFiber');
+const {getUpdateQueueExpirationTime} = require('ReactFiberUpdateQueue');
+const {NoWork} = require('ReactFiberExpirationTime');
export type FiberRoot = {
// Any additional information from the host associated with this root.
@@ -21,11 +25,35 @@ export type FiberRoot = {
current: Fiber,
// Determines if this root has already been added to the schedule for work.
isScheduled: boolean,
+ // A queue that represents times at which the root is blocked by a
+ // top-level update.
+ topLevelBlockers: UpdateQueue | null,
+ // The time at which this root completed.
+ completedAt: ExpirationTime,
+ // If this root completed, isBlocked indicates whether it's blocked
+ // from committing.
+ isBlocked: boolean,
+ // A queue of callbacks that fire once their corresponding expiration time
+ // has completed. Only fired once.
+ completionCallbacks: UpdateQueue | null,
+ // When set, indicates that all work in this tree with this time or earlier
+ // should be flushed by the end of the batch, as if it has task priority.
+ forceExpire: null | ExpirationTime,
// The work schedule is a linked list.
nextScheduledRoot: FiberRoot | null,
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
+ // Determines if we should attempt to hydrate on the initial mount
+ hydrate: boolean,
+};
+
+exports.topLevelBlockedAt = function(root: FiberRoot) {
+ const topLevelBlockers = root.topLevelBlockers;
+ if (topLevelBlockers === null) {
+ return NoWork;
+ }
+ return getUpdateQueueExpirationTime(topLevelBlockers);
};
exports.createFiberRoot = function(containerInfo: any): FiberRoot {
@@ -36,9 +64,15 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot {
current: uninitializedFiber,
containerInfo: containerInfo,
isScheduled: false,
+ completedAt: NoWork,
+ isBlocked: false,
+ completionCallbacks: null,
+ topLevelBlockers: null,
+ forceExpire: null,
nextScheduledRoot: null,
context: null,
pendingContext: null,
+ hydrate: true,
};
uninitializedFiber.stateNode = root;
return root;
diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js
index cc3800d129ea7..689f0aaf19ffd 100644
--- a/src/renderers/shared/fiber/ReactFiberScheduler.js
+++ b/src/renderers/shared/fiber/ReactFiberScheduler.js
@@ -15,6 +15,7 @@ import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig, Deadline} from 'ReactFiberReconciler';
import type {PriorityLevel} from 'ReactPriorityLevel';
import type {HydrationContext} from 'ReactFiberHydrationContext';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
export type CapturedError = {
componentName: ?string,
@@ -50,7 +51,7 @@ var ReactFiberHydrationContext = require('ReactFiberHydrationContext');
var {ReactCurrentOwner} = require('ReactGlobalSharedState');
var getComponentName = require('getComponentName');
-var {createWorkInProgress, largerPriority} = require('ReactFiber');
+var {createWorkInProgress} = require('ReactFiber');
var {onCommitRoot} = require('ReactFiberDevToolsHook');
var {
@@ -62,6 +63,13 @@ var {
OffscreenPriority,
} = require('ReactPriorityLevel');
+var {
+ Never,
+ msToExpirationTime,
+ priorityToExpirationTime,
+ expirationTimeToPriorityLevel,
+} = require('ReactFiberExpirationTime');
+
var {AsyncUpdates} = require('ReactTypeOfInternalContext');
var {
@@ -83,7 +91,13 @@ var {
ClassComponent,
} = require('ReactTypeOfWork');
-var {getUpdatePriority} = require('ReactFiberUpdateQueue');
+var {
+ getUpdateExpirationTime,
+ processUpdateQueue,
+ createUpdateQueue,
+ insertUpdateIntoQueue,
+ insertUpdateIntoFiber,
+} = require('ReactFiberUpdateQueue');
var {resetContext} = require('ReactFiberContext');
@@ -165,7 +179,6 @@ module.exports = function(
hostContext,
hydrationContext,
scheduleUpdate,
- getPriorityContext,
);
const {completeWork} = ReactFiberCompleteWork(
config,
@@ -181,16 +194,19 @@ module.exports = function(
commitDetachRef,
} = ReactFiberCommitWork(config, captureError);
const {
+ now,
scheduleDeferredCallback,
useSyncScheduling,
prepareForCommit,
resetAfterCommit,
} = config;
+ // Represents the current time in ms.
+ const startTime = now();
+ let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0);
+
// The priority level to use when scheduling an update. We use NoWork to
// represent the default priority.
- // TODO: Should we change this to an array instead of using the call stack?
- // Might be less confusing.
let priorityContext: PriorityLevel = NoWork;
// Keeps track of whether we're currently in a work loop.
@@ -208,13 +224,16 @@ module.exports = function(
// The next work in progress fiber that we're currently working on.
let nextUnitOfWork: Fiber | null = null;
- let nextPriorityLevel: PriorityLevel = NoWork;
+ // The time at which we're currently rendering work.
+ let nextRenderExpirationTime: ExpirationTime = NoWork;
// The next fiber with an effect that we're currently committing.
let nextEffect: Fiber | null = null;
let pendingCommit: Fiber | null = null;
+ let rootCompletionCallbackList: Array<() => mixed> | null = null;
+
// Linked list of roots with scheduled work on them.
let nextScheduledRoot: FiberRoot | null = null;
let lastScheduledRoot: FiberRoot | null = null;
@@ -251,14 +270,12 @@ module.exports = function(
resetHostContainer();
}
- // resetNextUnitOfWork mutates the current priority context. It is reset after
- // after the workLoop exits, so never call resetNextUnitOfWork from outside
- // the work loop.
function resetNextUnitOfWork() {
- // Clear out roots with no more work on them, or if they have uncaught errors
+ // Clear out roots with no more work on them
while (
nextScheduledRoot !== null &&
- nextScheduledRoot.current.pendingWorkPriority === NoWork
+ nextScheduledRoot.current.expirationTime === NoWork &&
+ nextScheduledRoot.completedAt === NoWork
) {
// Unschedule this root.
nextScheduledRoot.isScheduled = false;
@@ -270,7 +287,7 @@ module.exports = function(
if (nextScheduledRoot === lastScheduledRoot) {
nextScheduledRoot = null;
lastScheduledRoot = null;
- nextPriorityLevel = NoWork;
+ nextRenderExpirationTime = NoWork;
return null;
}
// Continue with the next root.
@@ -279,22 +296,23 @@ module.exports = function(
}
let root = nextScheduledRoot;
- let highestPriorityRoot = null;
- let highestPriorityLevel = NoWork;
+ let earliestExpirationRoot = null;
+ let earliestExpirationTime = NoWork;
while (root !== null) {
+ let rootExpirationTime = shouldWorkOnRoot(root);
if (
- root.current.pendingWorkPriority !== NoWork &&
- (highestPriorityLevel === NoWork ||
- highestPriorityLevel > root.current.pendingWorkPriority)
+ rootExpirationTime !== NoWork &&
+ (earliestExpirationTime === NoWork ||
+ earliestExpirationTime > rootExpirationTime)
) {
- highestPriorityLevel = root.current.pendingWorkPriority;
- highestPriorityRoot = root;
+ earliestExpirationTime = rootExpirationTime;
+ earliestExpirationRoot = root;
}
// We didn't find anything to do in this root, so let's try the next one.
root = root.nextScheduledRoot;
}
- if (highestPriorityRoot !== null) {
- nextPriorityLevel = highestPriorityLevel;
+ if (earliestExpirationRoot !== null) {
+ nextRenderExpirationTime = earliestExpirationTime;
// Before we start any new work, let's make sure that we have a fresh
// stack to work from.
// TODO: This call is buried a bit too deep. It would be nice to have
@@ -302,24 +320,90 @@ module.exports = function(
// unfortunately this is it.
resetContextStack();
- nextUnitOfWork = createWorkInProgress(
- highestPriorityRoot.current,
- highestPriorityLevel,
- );
- if (highestPriorityRoot !== nextRenderedTree) {
+ if (earliestExpirationRoot.completedAt === nextRenderExpirationTime) {
+ // If the root is already complete, reuse the existing work-in-progress.
+ // TODO: This is a limited version of resuming that only applies to
+ // the root, to account for the pathological case where a completed
+ // root must be completely restarted before it can commit. Once we
+ // implement resuming for real, this special branch shouldn't
+ // be neccessary.
+ nextUnitOfWork = earliestExpirationRoot.current.alternate;
+ invariant(
+ nextUnitOfWork !== null,
+ 'Expected a completed root to have a work-in-progress. This error ' +
+ 'is likely caused by a bug in React. Please file an issue.',
+ );
+ } else {
+ nextUnitOfWork = createWorkInProgress(
+ earliestExpirationRoot.current,
+ earliestExpirationTime,
+ );
+ }
+
+ earliestExpirationRoot.completedAt = NoWork;
+ earliestExpirationRoot.isBlocked = false;
+
+ if (earliestExpirationRoot !== nextRenderedTree) {
// We've switched trees. Reset the nested update counter.
nestedUpdateCount = 0;
- nextRenderedTree = highestPriorityRoot;
+ nextRenderedTree = earliestExpirationRoot;
}
return;
}
- nextPriorityLevel = NoWork;
+ nextRenderExpirationTime = NoWork;
nextUnitOfWork = null;
nextRenderedTree = null;
return;
}
+ // Indicates whether the root should be worked on. Not the same as whether a
+ // root has work, because work could be blocked.
+ // TODO: Find a better name for this function. It also schedules completion
+ // callbacks, if a root is blocked.
+ function shouldWorkOnRoot(root: FiberRoot): ExpirationTime {
+ const expirationTime = root.current.expirationTime;
+ if (expirationTime === NoWork) {
+ // There's no work in this tree.
+ return NoWork;
+ }
+ if (root.isBlocked) {
+ // We usually process completion callbacks right after a root is
+ // completed. But this root already completed, and it's possible that
+ // we received new completion callbacks since then.
+ processCompletionCallbacks(root, root.completedAt);
+ return NoWork;
+ }
+ return expirationTime;
+ }
+
+ function processCompletionCallbacks(
+ root: FiberRoot,
+ completedAt: ExpirationTime,
+ ) {
+ // Process pending completion callbacks so that they are called at
+ // the end of the current batch.
+ const completionCallbacks = root.completionCallbacks;
+ if (completionCallbacks !== null) {
+ processUpdateQueue(completionCallbacks, null, null, null, completedAt);
+ const callbackList = completionCallbacks.callbackList;
+ if (callbackList !== null) {
+ // Add new callbacks to list of completion callbacks
+ if (rootCompletionCallbackList === null) {
+ rootCompletionCallbackList = callbackList;
+ } else {
+ for (let i = 0; i < callbackList.length; i++) {
+ rootCompletionCallbackList.push(callbackList[i]);
+ }
+ }
+ completionCallbacks.callbackList = null;
+ if (completionCallbacks.first === null) {
+ root.completionCallbacks = null;
+ }
+ }
+ }
+ }
+
function commitAllHostEffects() {
while (nextEffect !== null) {
if (__DEV__) {
@@ -392,7 +476,6 @@ module.exports = function(
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
- // Use Task priority for lifecycle updates
if (effectTag & (Update | Callback)) {
if (__DEV__) {
recordEffect();
@@ -446,10 +529,9 @@ module.exports = function(
'in React. Please file an issue.',
);
- if (
- nextPriorityLevel === SynchronousPriority ||
- nextPriorityLevel === TaskPriority
- ) {
+ root.completedAt = NoWork;
+
+ if (nextRenderExpirationTime <= mostRecentCurrentTime) {
// Keep track of the number of iterations to prevent an infinite
// update loop.
nestedUpdateCount++;
@@ -583,35 +665,39 @@ module.exports = function(
commitPhaseBoundaries = null;
}
- // This tree is done. Reset the unit of work pointer to the next highest
- // priority root. If there's no more work left, the pointer is set to null.
+ // This tree is done. Reset the unit of work pointer to the root that
+ // expires soonest. If there's no work left, the pointer is set to null.
resetNextUnitOfWork();
}
- function resetWorkPriority(
+ function resetExpirationTime(
workInProgress: Fiber,
- renderPriority: PriorityLevel,
+ renderTime: ExpirationTime,
) {
- if (
- workInProgress.pendingWorkPriority !== NoWork &&
- workInProgress.pendingWorkPriority > renderPriority
- ) {
- // This was a down-prioritization. Don't bubble priority from children.
+ if (renderTime !== Never && workInProgress.expirationTime === Never) {
+ // The children of this component are hidden. Don't bubble their
+ // expiration times.
return;
}
- // Check for pending update priority.
- let newPriority = getUpdatePriority(workInProgress);
+ // Check for pending updates.
+ let newExpirationTime = getUpdateExpirationTime(workInProgress);
// TODO: Coroutines need to visit stateNode
+ // Bubble up the earliest expiration time.
let child = workInProgress.child;
while (child !== null) {
- // Ensure that remaining work priority bubbles up.
- newPriority = largerPriority(newPriority, child.pendingWorkPriority);
+ if (
+ child.expirationTime !== NoWork &&
+ (newExpirationTime === NoWork ||
+ newExpirationTime > child.expirationTime)
+ ) {
+ newExpirationTime = child.expirationTime;
+ }
child = child.sibling;
}
- workInProgress.pendingWorkPriority = newPriority;
+ workInProgress.expirationTime = newExpirationTime;
}
function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
@@ -624,7 +710,11 @@ module.exports = function(
if (__DEV__) {
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
}
- const next = completeWork(current, workInProgress, nextPriorityLevel);
+ const next = completeWork(
+ current,
+ workInProgress,
+ nextRenderExpirationTime,
+ );
if (__DEV__) {
ReactDebugCurrentFiber.resetCurrentFiber();
}
@@ -632,7 +722,7 @@ module.exports = function(
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
- resetWorkPriority(workInProgress, nextPriorityLevel);
+ resetExpirationTime(workInProgress, nextRenderExpirationTime);
if (next !== null) {
if (__DEV__) {
@@ -694,10 +784,15 @@ module.exports = function(
workInProgress = returnFiber;
continue;
} else {
- // We've reached the root. Mark the root as pending commit. Depending
- // on how much time we have left, we'll either commit it now or in
- // the next frame.
- pendingCommit = workInProgress;
+ // We've reached the root. Mark it as complete.
+ const root = workInProgress.stateNode;
+ root.completedAt = nextRenderExpirationTime;
+ // If the root isn't blocked, it's ready to commit. If it is blocked,
+ // we'll come back to it later.
+ if (!root.isBlocked) {
+ pendingCommit = workInProgress;
+ }
+ processCompletionCallbacks(root, nextRenderExpirationTime);
return null;
}
}
@@ -720,7 +815,7 @@ module.exports = function(
startWorkTimer(workInProgress);
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
}
- let next = beginWork(current, workInProgress, nextPriorityLevel);
+ let next = beginWork(current, workInProgress, nextRenderExpirationTime);
if (__DEV__) {
ReactDebugCurrentFiber.resetCurrentFiber();
}
@@ -750,7 +845,11 @@ module.exports = function(
startWorkTimer(workInProgress);
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
}
- let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
+ let next = beginFailedWork(
+ current,
+ workInProgress,
+ nextRenderExpirationTime,
+ );
if (__DEV__) {
ReactDebugCurrentFiber.resetCurrentFiber();
}
@@ -785,7 +884,8 @@ module.exports = function(
if (
capturedErrors !== null &&
capturedErrors.size > 0 &&
- nextPriorityLevel === TaskPriority
+ nextRenderExpirationTime !== NoWork &&
+ nextRenderExpirationTime <= mostRecentCurrentTime
) {
while (nextUnitOfWork !== null) {
if (hasCapturedError(nextUnitOfWork)) {
@@ -795,20 +895,17 @@ module.exports = function(
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork === null) {
- invariant(
- pendingCommit !== null,
- 'Should have a pending commit. This error is likely caused by ' +
- 'a bug in React. Please file an issue.',
- );
- // We just completed a root. Commit it now.
- priorityContext = TaskPriority;
- commitAllWork(pendingCommit);
- priorityContext = nextPriorityLevel;
-
+ if (pendingCommit !== null) {
+ // We just completed a root. Commit it now.
+ commitAllWork(pendingCommit);
+ } else {
+ resetNextUnitOfWork();
+ }
if (
capturedErrors === null ||
capturedErrors.size === 0 ||
- nextPriorityLevel !== TaskPriority
+ nextRenderExpirationTime === NoWork ||
+ nextRenderExpirationTime > mostRecentCurrentTime
) {
// There are no more unhandled errors. We can exit this special
// work loop. If there's still additional work, we'll perform it
@@ -821,56 +918,99 @@ module.exports = function(
}
}
+ function shouldContinueWorking(
+ minPriorityLevel: PriorityLevel,
+ nextPriorityLevel: PriorityLevel,
+ deadline: Deadline | null,
+ ): boolean {
+ // There might be work left. Depending on the priority, we should
+ // either perform it now or schedule a callback to perform it later.
+ switch (nextPriorityLevel) {
+ case SynchronousPriority:
+ case TaskPriority:
+ // We have remaining synchronous or task work. Keep performing it,
+ // regardless of whether we're inside a callback.
+ if (nextPriorityLevel <= minPriorityLevel) {
+ return true;
+ }
+ return false;
+ case HighPriority:
+ case LowPriority:
+ case OffscreenPriority:
+ // We have remaining async work.
+ if (deadline === null) {
+ // We're not inside a callback. Exit and perform the work during
+ // the next callback.
+ return false;
+ }
+ // We are inside a callback.
+ if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) {
+ // We still have time. Keep working.
+ return true;
+ }
+ // We've run out of time. Exit.
+ return false;
+ case NoWork:
+ // No work left. We can exit.
+ return false;
+ default:
+ invariant(
+ false,
+ 'Switch statement should be exhuastive. ' +
+ 'This error is likely caused by a bug in React. Please file an issue.',
+ );
+ }
+ }
+
function workLoop(
minPriorityLevel: PriorityLevel,
deadline: Deadline | null,
) {
if (pendingCommit !== null) {
- priorityContext = TaskPriority;
commitAllWork(pendingCommit);
handleCommitPhaseErrors();
} else if (nextUnitOfWork === null) {
resetNextUnitOfWork();
}
- if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) {
- return;
- }
-
- // During the render phase, updates should have the same priority at which
- // we're rendering.
- priorityContext = nextPriorityLevel;
+ let nextPriorityLevel = expirationTimeToPriorityLevel(
+ recalculateCurrentTime(),
+ nextRenderExpirationTime,
+ );
- loop: do {
- if (nextPriorityLevel <= TaskPriority) {
- // Flush all synchronous and task work.
+ while (
+ shouldContinueWorking(minPriorityLevel, nextPriorityLevel, deadline)
+ ) {
+ if (nextRenderExpirationTime <= mostRecentCurrentTime) {
+ // Flush all expired work.
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (nextUnitOfWork === null) {
- invariant(
- pendingCommit !== null,
- 'Should have a pending commit. This error is likely caused by ' +
- 'a bug in React. Please file an issue.',
+ if (pendingCommit !== null) {
+ // We just completed a root. Commit it now.
+ commitAllWork(pendingCommit);
+ // Clear any errors that were scheduled during the commit phase.
+ handleCommitPhaseErrors();
+ } else {
+ resetNextUnitOfWork();
+ }
+ // The render time may have changed. Check again.
+ nextPriorityLevel = expirationTimeToPriorityLevel(
+ recalculateCurrentTime(),
+ nextRenderExpirationTime,
);
- // We just completed a root. Commit it now.
- priorityContext = TaskPriority;
- commitAllWork(pendingCommit);
- priorityContext = nextPriorityLevel;
- // Clear any errors that were scheduled during the commit phase.
- handleCommitPhaseErrors();
- // The priority level may have changed. Check again.
if (
nextPriorityLevel === NoWork ||
nextPriorityLevel > minPriorityLevel ||
nextPriorityLevel > TaskPriority
) {
- // The priority level does not match.
+ // We've completed all the expired work.
break;
}
}
}
} else if (deadline !== null) {
- // Flush asynchronous work until the deadline expires.
+ // Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !deadlineHasExpired) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
@@ -879,26 +1019,26 @@ module.exports = function(
// omit either of the checks in the following condition, but we need
// both to satisfy Flow.
if (nextUnitOfWork === null) {
- invariant(
- pendingCommit !== null,
- 'Should have a pending commit. This error is likely caused by ' +
- 'a bug in React. Please file an issue.',
- );
- // We just completed a root. If we have time, commit it now.
- // Otherwise, we'll commit it in the next frame.
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
- priorityContext = TaskPriority;
- commitAllWork(pendingCommit);
- priorityContext = nextPriorityLevel;
- // Clear any errors that were scheduled during the commit phase.
- handleCommitPhaseErrors();
- // The priority level may have changed. Check again.
+ if (pendingCommit !== null) {
+ // We just completed a root. Commit it now.
+ commitAllWork(pendingCommit);
+ // Clear any errors that were scheduled during the commit phase.
+ handleCommitPhaseErrors();
+ } else {
+ resetNextUnitOfWork();
+ }
+ // The render time may have changed. Check again.
+ nextPriorityLevel = expirationTimeToPriorityLevel(
+ recalculateCurrentTime(),
+ nextRenderExpirationTime,
+ );
if (
nextPriorityLevel === NoWork ||
nextPriorityLevel > minPriorityLevel ||
- nextPriorityLevel < HighPriority
+ nextPriorityLevel <= TaskPriority
) {
- // The priority level does not match.
+ // We've completed all the async work.
break;
}
} else {
@@ -910,45 +1050,7 @@ module.exports = function(
}
}
}
-
- // There might be work left. Depending on the priority, we should
- // either perform it now or schedule a callback to perform it later.
- switch (nextPriorityLevel) {
- case SynchronousPriority:
- case TaskPriority:
- // We have remaining synchronous or task work. Keep performing it,
- // regardless of whether we're inside a callback.
- if (nextPriorityLevel <= minPriorityLevel) {
- continue loop;
- }
- break loop;
- case HighPriority:
- case LowPriority:
- case OffscreenPriority:
- // We have remaining async work.
- if (deadline === null) {
- // We're not inside a callback. Exit and perform the work during
- // the next callback.
- break loop;
- }
- // We are inside a callback.
- if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) {
- // We still have time. Keep working.
- continue loop;
- }
- // We've run out of time. Exit.
- break loop;
- case NoWork:
- // No work left. We can exit.
- break loop;
- default:
- invariant(
- false,
- 'Switch statement should be exhuastive. ' +
- 'This error is likely caused by a bug in React. Please file an issue.',
- );
- }
- } while (true);
+ }
}
function performWorkCatchBlock(
@@ -988,9 +1090,15 @@ module.exports = function(
);
isPerformingWork = true;
- // The priority context changes during the render phase. We'll need to
- // reset it at the end.
+ rootCompletionCallbackList = null;
+
+ // Updates that occur during the commit phase should have task priority
+ // by default. (Render phase updates are special; getPriorityContext
+ // accounts for their behavior.)
const previousPriorityContext = priorityContext;
+ priorityContext = TaskPriority;
+
+ nestedUpdateCount = 0;
let didError = false;
let error = null;
@@ -1076,19 +1184,21 @@ module.exports = function(
break;
}
- // Reset the priority context to its previous value.
- priorityContext = previousPriorityContext;
-
// If we're inside a callback, set this to false, since we just flushed it.
if (deadline !== null) {
isCallbackScheduled = false;
}
// If there's remaining async work, make sure we schedule another callback.
- if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) {
+ if (
+ nextRenderExpirationTime > mostRecentCurrentTime &&
+ !isCallbackScheduled
+ ) {
scheduleDeferredCallback(performDeferredWork);
isCallbackScheduled = true;
}
+ priorityContext = previousPriorityContext;
+
const errorToThrow = firstUncaughtError;
// We're done performing work. Time to clean up.
@@ -1109,6 +1219,15 @@ module.exports = function(
if (errorToThrow !== null) {
throw errorToThrow;
}
+
+ // Call completion callbacks. These callbacks may call performWork. This
+ // is the one place where recursion is allowed.
+ if (rootCompletionCallbackList !== null) {
+ const list = rootCompletionCallbackList;
+ for (let i = 0; i < list.length; i++) {
+ list[i]();
+ }
+ }
}
// Returns the boundary that captured the error, or null if the error is ignored
@@ -1353,32 +1472,41 @@ module.exports = function(
}
}
- function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) {
- if (priorityLevel === NoWork) {
- return;
- }
-
- if (!root.isScheduled) {
- root.isScheduled = true;
- if (lastScheduledRoot) {
- // Schedule ourselves to the end.
- lastScheduledRoot.nextScheduledRoot = root;
- lastScheduledRoot = root;
- } else {
- // We're the only work scheduled.
- nextScheduledRoot = root;
- lastScheduledRoot = root;
- }
- }
+ function scheduleUpdate(
+ fiber: Fiber,
+ partialState: mixed,
+ callback: (() => mixed) | null,
+ isReplace: boolean,
+ isForced: boolean,
+ ) {
+ const priorityLevel = getPriorityContext(fiber, false);
+ const currentTime = recalculateCurrentTime();
+ const expirationTime = getExpirationTimeForPriority(
+ currentTime,
+ priorityLevel,
+ );
+ const update = {
+ priorityLevel,
+ expirationTime,
+ partialState,
+ callback,
+ isReplace,
+ isForced,
+ nextCallback: null,
+ isTopLevelUnmount: false,
+ next: null,
+ };
+ insertUpdateIntoFiber(fiber, update, currentTime);
+ scheduleWork(fiber, expirationTime);
}
- function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) {
- return scheduleUpdateImpl(fiber, priorityLevel, false);
+ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
+ return scheduleWorkImpl(fiber, expirationTime, false);
}
- function scheduleUpdateImpl(
+ function scheduleWorkImpl(
fiber: Fiber,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
isErrorRecovery: boolean,
) {
if (__DEV__) {
@@ -1396,7 +1524,7 @@ module.exports = function(
);
}
- if (!isPerformingWork && priorityLevel <= nextPriorityLevel) {
+ if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) {
// We must reset the current unit of work pointer so that we restart the
// search from the root during the next tick, in case there is now higher
// priority work somewhere earlier than before.
@@ -1411,38 +1539,52 @@ module.exports = function(
}
let node = fiber;
- let shouldContinue = true;
- while (node !== null && shouldContinue) {
- // Walk the parent path to the root and update each node's priority. Once
- // we reach a node whose priority matches (and whose alternate's priority
- // matches) we can exit safely knowing that the rest of the path is correct.
- shouldContinue = false;
+ while (node !== null) {
if (
- node.pendingWorkPriority === NoWork ||
- node.pendingWorkPriority > priorityLevel
+ node.expirationTime === NoWork ||
+ node.expirationTime > expirationTime
) {
- // Priority did not match. Update and keep going.
- shouldContinue = true;
- node.pendingWorkPriority = priorityLevel;
+ node.expirationTime = expirationTime;
}
if (node.alternate !== null) {
if (
- node.alternate.pendingWorkPriority === NoWork ||
- node.alternate.pendingWorkPriority > priorityLevel
+ node.alternate.expirationTime === NoWork ||
+ node.alternate.expirationTime > expirationTime
) {
- // Priority did not match. Update and keep going.
- shouldContinue = true;
- node.alternate.pendingWorkPriority = priorityLevel;
+ node.alternate.expirationTime = expirationTime;
}
}
if (node.return === null) {
if (node.tag === HostRoot) {
const root: FiberRoot = (node.stateNode: any);
- scheduleRoot(root, priorityLevel);
+
+ // Add the root to the work schedule.
+ if (expirationTime !== NoWork) {
+ root.isBlocked = false;
+ if (!root.isScheduled) {
+ root.isScheduled = true;
+ if (lastScheduledRoot) {
+ // Schedule ourselves to the end.
+ lastScheduledRoot.nextScheduledRoot = root;
+ lastScheduledRoot = root;
+ } else {
+ // We're the only work scheduled.
+ nextScheduledRoot = root;
+ lastScheduledRoot = root;
+ }
+ }
+ }
+
+ // If we're not current performing work, we need to either start
+ // working now (if the update is synchronous) or schedule a callback
+ // to perform work later.
if (!isPerformingWork) {
+ const priorityLevel = expirationTimeToPriorityLevel(
+ mostRecentCurrentTime,
+ expirationTime,
+ );
switch (priorityLevel) {
case SynchronousPriority:
- // Perform this update now.
if (isUnbatchingUpdates) {
// We're inside unbatchedUpdates, which is inside either
// batchedUpdates or a lifecycle. We should only flush
@@ -1454,14 +1596,12 @@ module.exports = function(
}
break;
case TaskPriority:
- invariant(
- isBatchingUpdates,
- 'Task updates can only be scheduled as a nested update or ' +
- 'inside batchedUpdates.',
- );
+ if (!isPerformingWork && !isBatchingUpdates) {
+ performWork(TaskPriority, null);
+ }
break;
default:
- // Schedule a callback to perform the work later.
+ // This update is async. Schedule a callback.
if (!isCallbackScheduled) {
scheduleDeferredCallback(performDeferredWork);
isCallbackScheduled = true;
@@ -1481,10 +1621,47 @@ module.exports = function(
}
}
+ function scheduleCompletionCallback(
+ root: FiberRoot,
+ callback: () => mixed,
+ expirationTime: ExpirationTime,
+ ) {
+ // Add callback to queue of callbacks on the root. It will be called once
+ // the root completes at the corresponding expiration time.
+ const update = {
+ priorityLevel: null,
+ expirationTime,
+ partialState: null,
+ callback,
+ isReplace: false,
+ isForced: false,
+ isTopLevelUnmount: false,
+ next: null,
+ };
+ const currentTime = recalculateCurrentTime();
+ if (root.completionCallbacks === null) {
+ root.completionCallbacks = createUpdateQueue();
+ }
+ insertUpdateIntoQueue(root.completionCallbacks, update, currentTime);
+ if (expirationTime === root.completedAt) {
+ // The tree already completed at this expiration time. Resolve the
+ // callback synchronously.
+ performWork(TaskPriority, null);
+ } else {
+ scheduleWork(root.current, expirationTime);
+ }
+ }
+
function getPriorityContext(
fiber: Fiber,
forceAsync: boolean,
- ): PriorityLevel {
+ ): PriorityLevel | null {
+ if (isPerformingWork && !isCommitting) {
+ // Updates during the render phase should expire at the same time as
+ // the work that is being rendered. Return null to indicate.
+ return null;
+ }
+
let priorityLevel = priorityContext;
if (priorityLevel === NoWork) {
if (
@@ -1509,8 +1686,56 @@ module.exports = function(
return priorityLevel;
}
+ function getExpirationTimeForPriority(
+ currentTime: ExpirationTime,
+ priorityLevel: PriorityLevel | null,
+ ): ExpirationTime {
+ if (priorityLevel === null) {
+ // A priorityLevel of null indicates that this update should expire at
+ // the same time as whatever is currently being rendered.
+ return nextRenderExpirationTime;
+ }
+ return priorityToExpirationTime(currentTime, priorityLevel);
+ }
+
function scheduleErrorRecovery(fiber: Fiber) {
- scheduleUpdateImpl(fiber, TaskPriority, true);
+ const taskTime = getExpirationTimeForPriority(
+ mostRecentCurrentTime,
+ TaskPriority,
+ );
+ scheduleWorkImpl(fiber, taskTime, true);
+ }
+
+ function recalculateCurrentTime(): ExpirationTime {
+ if (nextRenderedTree !== null) {
+ // Check if the current root is being force expired.
+ const forceExpire = nextRenderedTree.forceExpire;
+ if (forceExpire !== null) {
+ // Override the current time with the `forceExpire` time. This has the
+ // effect of expiring all work up to and including that time.
+ mostRecentCurrentTime = forceExpire;
+ return forceExpire;
+ }
+ }
+ // Subtract initial time so it fits inside 32bits
+ const ms = now() - startTime;
+ mostRecentCurrentTime = msToExpirationTime(ms);
+ return mostRecentCurrentTime;
+ }
+
+ function expireWork(root: FiberRoot, expirationTime: ExpirationTime): void {
+ invariant(
+ !isPerformingWork,
+ 'Cannot commit while already performing work.',
+ );
+ root.forceExpire = expirationTime;
+ root.isBlocked = false;
+ try {
+ performWork(TaskPriority, null);
+ } finally {
+ root.forceExpire = null;
+ recalculateCurrentTime();
+ }
}
function batchedUpdates(fn: (a: A) => R, a: A): R {
@@ -1573,8 +1798,12 @@ module.exports = function(
}
return {
- scheduleUpdate: scheduleUpdate,
+ scheduleWork: scheduleWork,
+ scheduleCompletionCallback: scheduleCompletionCallback,
getPriorityContext: getPriorityContext,
+ recalculateCurrentTime: recalculateCurrentTime,
+ getExpirationTimeForPriority: getExpirationTimeForPriority,
+ expireWork: expireWork,
batchedUpdates: batchedUpdates,
unbatchedUpdates: unbatchedUpdates,
flushSync: flushSync,
diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
index 39f483b7d78df..f515097716978 100644
--- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
@@ -12,18 +12,14 @@
import type {Fiber} from 'ReactFiber';
import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect');
-const {
- NoWork,
- SynchronousPriority,
- TaskPriority,
-} = require('ReactPriorityLevel');
+const {NoWork} = require('ReactFiberExpirationTime');
const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
-const invariant = require('fbjs/lib/invariant');
if (__DEV__) {
var warning = require('fbjs/lib/warning');
}
@@ -32,17 +28,17 @@ type PartialState =
| $Subtype
| ((prevState: State, props: Props) => $Subtype);
-// Callbacks are not validated until invocation
-type Callback = mixed;
+type Callback = () => mixed;
-type Update = {
- priorityLevel: PriorityLevel,
- partialState: PartialState,
+export type Update = {
+ priorityLevel: PriorityLevel | null,
+ expirationTime: ExpirationTime,
+ partialState: PartialState,
callback: Callback | null,
isReplace: boolean,
isForced: boolean,
isTopLevelUnmount: boolean,
- next: Update | null,
+ next: Update | null,
};
// Singly linked-list of updates. When an update is scheduled, it is added to
@@ -56,9 +52,9 @@ type Update = {
// The work-in-progress queue is always a subset of the current queue.
//
// When the tree is committed, the work-in-progress becomes the current.
-export type UpdateQueue = {
- first: Update | null,
- last: Update | null,
+export type UpdateQueue = {
+ first: Update | null,
+ last: Update | null,
hasForceUpdate: boolean,
callbackList: null | Array,
@@ -69,27 +65,8 @@ export type UpdateQueue = {
let _queue1;
let _queue2;
-function comparePriority(a: PriorityLevel, b: PriorityLevel): number {
- // When comparing update priorities, treat sync and Task work as equal.
- // TODO: Could we avoid the need for this by always coercing sync priority
- // to Task when scheduling an update?
- if (
- (a === TaskPriority || a === SynchronousPriority) &&
- (b === TaskPriority || b === SynchronousPriority)
- ) {
- return 0;
- }
- if (a === NoWork && b !== NoWork) {
- return -255;
- }
- if (a !== NoWork && b === NoWork) {
- return 255;
- }
- return a - b;
-}
-
-function createUpdateQueue(): UpdateQueue {
- const queue: UpdateQueue = {
+function createUpdateQueue(): UpdateQueue {
+ const queue: UpdateQueue = {
first: null,
last: null,
hasForceUpdate: false,
@@ -100,10 +77,12 @@ function createUpdateQueue(): UpdateQueue {
}
return queue;
}
+exports.createUpdateQueue = createUpdateQueue;
-function cloneUpdate(update: Update): Update {
+function cloneUpdate(update: Update): Update {
return {
priorityLevel: update.priorityLevel,
+ expirationTime: update.expirationTime,
partialState: update.partialState,
callback: update.callback,
isReplace: update.isReplace,
@@ -113,14 +92,33 @@ function cloneUpdate(update: Update): Update {
};
}
-function insertUpdateIntoQueue(
- queue: UpdateQueue,
- update: Update,
- insertAfter: Update | null,
- insertBefore: Update | null,
+const COALESCENCE_THRESHOLD: ExpirationTime = 10;
+
+function insertUpdateIntoPosition(
+ queue: UpdateQueue,
+ update: Update,
+ insertAfter: Update | null,
+ insertBefore: Update | null,
+ currentTime: ExpirationTime,
) {
if (insertAfter !== null) {
insertAfter.next = update;
+ // If we receive multiple updates to the same fiber at the same priority
+ // level, we coalesce them by assigning the same expiration time, so that
+ // they all flush at the same time. Because this causes an interruption, it
+ // could lead to starvation, so we stop coalescing once the time until the
+ // expiration time reaches a certain threshold.
+ if (
+ // Only coalesce if a priority level is specified
+ update.priorityLevel !== null &&
+ insertAfter !== null &&
+ insertAfter.priorityLevel === update.priorityLevel
+ ) {
+ const coalescedTime = insertAfter.expirationTime;
+ if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) {
+ update.expirationTime = coalescedTime;
+ }
+ }
} else {
// This is the first item in the queue.
update.next = queue.first;
@@ -137,14 +135,14 @@ function insertUpdateIntoQueue(
// Returns the update after which the incoming update should be inserted into
// the queue, or null if it should be inserted at beginning.
-function findInsertionPosition(queue, update): Update | null {
- const priorityLevel = update.priorityLevel;
+function findInsertionPosition(
+ queue: UpdateQueue,
+ update: Update,
+): Update | null {
+ const expirationTime = update.expirationTime;
let insertAfter = null;
let insertBefore = null;
- if (
- queue.last !== null &&
- comparePriority(queue.last.priorityLevel, priorityLevel) <= 0
- ) {
+ if (queue.last !== null && queue.last.expirationTime <= expirationTime) {
// Fast path for the common case where the update should be inserted at
// the end of the queue.
insertAfter = queue.last;
@@ -152,7 +150,7 @@ function findInsertionPosition(queue, update): Update | null {
insertBefore = queue.first;
while (
insertBefore !== null &&
- comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0
+ insertBefore.expirationTime <= expirationTime
) {
insertAfter = insertBefore;
insertBefore = insertBefore.next;
@@ -213,7 +211,11 @@ function ensureUpdateQueues(fiber: Fiber) {
// we shouldn't make a copy.
//
// If the update is cloned, it returns the cloned update.
-function insertUpdate(fiber: Fiber, update: Update): Update | null {
+function insertUpdateIntoFiber(
+ fiber: Fiber,
+ update: Update,
+ currentTime: ExpirationTime,
+): Update | null {
// We'll have at least one and at most two distinct update queues.
ensureUpdateQueues(fiber);
const queue1 = _queue1;
@@ -240,7 +242,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
if (queue2 === null) {
// If there's no alternate queue, there's nothing else to do but insert.
- insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
+ insertUpdateIntoPosition(
+ queue1,
+ update,
+ insertAfter1,
+ insertBefore1,
+ currentTime,
+ );
return null;
}
@@ -252,7 +260,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
// Now we can insert into the first queue. This must come after finding both
// insertion positions because it mutates the list.
- insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
+ insertUpdateIntoPosition(
+ queue1,
+ update,
+ insertAfter1,
+ insertBefore1,
+ currentTime,
+ );
// See if the insertion positions are equal. Be careful to only compare
// non-null values.
@@ -275,68 +289,36 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
// The insertion positions are different, so we need to clone the update and
// insert the clone into the alternate queue.
const update2 = cloneUpdate(update);
- insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
+ insertUpdateIntoPosition(
+ queue2,
+ update2,
+ insertAfter2,
+ insertBefore2,
+ currentTime,
+ );
return update2;
}
}
+exports.insertUpdateIntoFiber = insertUpdateIntoFiber;
-function addUpdate(
- fiber: Fiber,
- partialState: PartialState | null,
- callback: mixed,
- priorityLevel: PriorityLevel,
-): void {
- const update = {
- priorityLevel,
- partialState,
- callback,
- isReplace: false,
- isForced: false,
- isTopLevelUnmount: false,
- next: null,
- };
- insertUpdate(fiber, update);
-}
-exports.addUpdate = addUpdate;
-
-function addReplaceUpdate(
- fiber: Fiber,
- state: any | null,
- callback: Callback | null,
- priorityLevel: PriorityLevel,
-): void {
- const update = {
- priorityLevel,
- partialState: state,
- callback,
- isReplace: true,
- isForced: false,
- isTopLevelUnmount: false,
- next: null,
- };
- insertUpdate(fiber, update);
-}
-exports.addReplaceUpdate = addReplaceUpdate;
-
-function addForceUpdate(
- fiber: Fiber,
- callback: Callback | null,
- priorityLevel: PriorityLevel,
-): void {
- const update = {
- priorityLevel,
- partialState: null,
- callback,
- isReplace: false,
- isForced: true,
- isTopLevelUnmount: false,
- next: null,
- };
- insertUpdate(fiber, update);
+function insertUpdateIntoQueue(
+ queue: UpdateQueue,
+ update: Update,
+ currentTime: ExpirationTime,
+) {
+ const insertAfter = findInsertionPosition(queue, update);
+ const insertBefore = insertAfter !== null ? insertAfter.next : null;
+ insertUpdateIntoPosition(
+ queue,
+ update,
+ insertAfter,
+ insertBefore,
+ currentTime,
+ );
}
-exports.addForceUpdate = addForceUpdate;
+exports.insertUpdateIntoQueue = insertUpdateIntoQueue;
-function getUpdatePriority(fiber: Fiber): PriorityLevel {
+function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
return NoWork;
@@ -344,81 +326,35 @@ function getUpdatePriority(fiber: Fiber): PriorityLevel {
if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
return NoWork;
}
- return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork;
+ return getUpdateQueueExpirationTime(updateQueue);
}
-exports.getUpdatePriority = getUpdatePriority;
+exports.getUpdateExpirationTime = getUpdateExpirationTime;
-function addTopLevelUpdate(
- fiber: Fiber,
- partialState: PartialState,
- callback: Callback | null,
- priorityLevel: PriorityLevel,
-): void {
- const isTopLevelUnmount = partialState.element === null;
-
- const update = {
- priorityLevel,
- partialState,
- callback,
- isReplace: false,
- isForced: false,
- isTopLevelUnmount,
- next: null,
- };
- const update2 = insertUpdate(fiber, update);
-
- if (isTopLevelUnmount) {
- // TODO: Redesign the top-level mount/update/unmount API to avoid this
- // special case.
- const queue1 = _queue1;
- const queue2 = _queue2;
-
- // Drop all updates that are lower-priority, so that the tree is not
- // remounted. We need to do this for both queues.
- if (queue1 !== null && update.next !== null) {
- update.next = null;
- queue1.last = update;
- }
- if (queue2 !== null && update2 !== null && update2.next !== null) {
- update2.next = null;
- queue2.last = update;
- }
- }
+function getUpdateQueueExpirationTime(
+ updateQueue: UpdateQueue,
+): ExpirationTime {
+ return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork;
}
-exports.addTopLevelUpdate = addTopLevelUpdate;
+exports.getUpdateQueueExpirationTime = getUpdateQueueExpirationTime;
function getStateFromUpdate(update, instance, prevState, props) {
const partialState = update.partialState;
if (typeof partialState === 'function') {
const updateFn = partialState;
+ // $FlowFixMe - Idk how to type State correctly.
return updateFn.call(instance, prevState, props);
} else {
return partialState;
}
}
-function beginUpdateQueue(
- current: Fiber | null,
- workInProgress: Fiber,
- queue: UpdateQueue,
- instance: any,
- prevState: any,
- props: any,
- priorityLevel: PriorityLevel,
-): any {
- if (current !== null && current.updateQueue === queue) {
- // We need to create a work-in-progress queue, by cloning the current queue.
- const currentQueue = queue;
- queue = workInProgress.updateQueue = {
- first: currentQueue.first,
- last: currentQueue.last,
- // These fields are no longer valid because they were already committed.
- // Reset them.
- callbackList: null,
- hasForceUpdate: false,
- };
- }
-
+function processUpdateQueue(
+ queue: UpdateQueue,
+ instance: mixed,
+ prevState: State,
+ props: mixed,
+ renderExpirationTime: ExpirationTime,
+): State {
if (__DEV__) {
// Set this flag so we can warn if setState is called inside the update
// function of another setState.
@@ -434,10 +370,7 @@ function beginUpdateQueue(
let state = prevState;
let dontMutatePrevState = true;
let update = queue.first;
- while (
- update !== null &&
- comparePriority(update.priorityLevel, priorityLevel) <= 0
- ) {
+ while (update !== null && update.expirationTime <= renderExpirationTime) {
// Remove each update from the queue right before it is processed. That way
// if setState is called from inside an updater function, the new update
// will be inserted in the correct position.
@@ -454,6 +387,7 @@ function beginUpdateQueue(
partialState = getStateFromUpdate(update, instance, state, props);
if (partialState) {
if (dontMutatePrevState) {
+ // $FlowFixMe - Idk how to type State properly.
state = Object.assign({}, state, partialState);
} else {
state = Object.assign(state, partialState);
@@ -472,7 +406,6 @@ function beginUpdateQueue(
) {
callbackList = callbackList !== null ? callbackList : [];
callbackList.push(update.callback);
- workInProgress.effectTag |= CallbackEffect;
}
update = update.next;
}
@@ -480,11 +413,6 @@ function beginUpdateQueue(
queue.callbackList = callbackList;
queue.hasForceUpdate = hasForceUpdate;
- if (queue.first === null && callbackList === null && !hasForceUpdate) {
- // The queue is empty and there are no callbacks. We can reset it.
- workInProgress.updateQueue = null;
- }
-
if (__DEV__) {
// No longer processing.
queue.isProcessing = false;
@@ -492,30 +420,49 @@ function beginUpdateQueue(
return state;
}
-exports.beginUpdateQueue = beginUpdateQueue;
+exports.processUpdateQueue = processUpdateQueue;
-function commitCallbacks(
- finishedWork: Fiber,
- queue: UpdateQueue,
- context: mixed,
-) {
- const callbackList = queue.callbackList;
- if (callbackList === null) {
- return;
+function beginUpdateQueue(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ queue: UpdateQueue,
+ instance: any,
+ prevState: any,
+ props: any,
+ renderExpirationTime: ExpirationTime,
+): State {
+ if (current !== null && current.updateQueue === queue) {
+ // We need to create a work-in-progress queue, by cloning the current queue.
+ const currentQueue = queue;
+ queue = workInProgress.updateQueue = {
+ first: currentQueue.first,
+ last: currentQueue.last,
+ // These fields are no longer valid because they were already committed.
+ // Reset them.
+ callbackList: null,
+ hasForceUpdate: false,
+ };
}
- // Set the list to null to make sure they don't get called more than once.
- queue.callbackList = null;
-
- for (let i = 0; i < callbackList.length; i++) {
- const callback = callbackList[i];
- invariant(
- typeof callback === 'function',
- 'Invalid argument passed as callback. Expected a function. Instead ' +
- 'received: %s',
- callback,
- );
- callback.call(context);
+ const state = processUpdateQueue(
+ queue,
+ instance,
+ prevState,
+ props,
+ renderExpirationTime,
+ );
+
+ const updatedQueue = workInProgress.updateQueue;
+ if (updatedQueue !== null) {
+ const callbackList = updatedQueue.callbackList;
+ if (callbackList !== null) {
+ workInProgress.effectTag |= CallbackEffect;
+ } else if (updatedQueue.first === null && !updatedQueue.hasForceUpdate) {
+ // The queue is empty. We can reset it.
+ workInProgress.updateQueue = null;
+ }
}
+
+ return state;
}
-exports.commitCallbacks = commitCallbacks;
+exports.beginUpdateQueue = beginUpdateQueue;
diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js
new file mode 100644
index 0000000000000..7b9121f19a209
--- /dev/null
+++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var React;
+var ReactNoop;
+
+describe('ReactExpiration', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ });
+
+ function span(prop) {
+ return {type: 'span', children: [], prop};
+ }
+
+ it('increases priority of updates as time progresses', () => {
+ ReactNoop.render();
+
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Nothing has expired yet because time hasn't advanced.
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Advance by 300ms, not enough to expire the low pri update.
+ ReactNoop.expire(300);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Advance by another second. Now the update should expire and flush.
+ ReactNoop.expire(1000);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span('done')]);
+ });
+
+ it('coalesces updates to the same component', () => {
+ const foos = [];
+ class Foo extends React.Component {
+ constructor() {
+ super();
+ this.state = {step: 0};
+ foos.push(this);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ ReactNoop.render([, ]);
+ ReactNoop.flush();
+ const [a, b] = foos;
+
+ a.setState({step: 1});
+
+ // Advance time by 500ms.
+ ReactNoop.expire(500);
+
+ // Update A again. This update should coalesce with the previous update.
+ a.setState({step: 2});
+ // Update B. This is the first update, so it has nothing to coalesce with.
+ b.setState({step: 1});
+
+ // Advance time. This should be enough to flush both updates to A, but not
+ // the update to B. If only the first update to A flushes, but not the
+ // second, then it wasn't coalesced properly.
+ ReactNoop.expire(600);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]);
+
+ // Now expire the update to B.
+ ReactNoop.expire(500);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(2), span(1)]);
+ });
+
+ it('stops coalescing after a certain threshold', () => {
+ let instance;
+ class Foo extends React.Component {
+ state = {step: 0};
+ render() {
+ instance = this;
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ instance.setState({step: 1});
+
+ // Advance time by 500 ms.
+ ReactNoop.expire(500);
+
+ // Update again. This update should coalesce with the previous update.
+ instance.setState({step: 2});
+
+ // Advance time by 480ms. Not enough to expire the updates.
+ ReactNoop.expire(480);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
+
+ // Update again. This update should NOT be coalesced, because the
+ // previous updates have almost expired.
+ instance.setState({step: 3});
+
+ // Advance time. This should expire the first two updates,
+ // but not the third.
+ ReactNoop.expire(500);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(2)]);
+
+ // Now expire the remaining update.
+ ReactNoop.expire(1000);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(3)]);
+ });
+});
diff --git a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
index 7c00d20f50f76..60416632715f5 100644
--- a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
+++ b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
@@ -46,6 +46,9 @@ describe('ReactFiberHostContext', () => {
appendChildToContainer: function() {
return null;
},
+ now: function() {
+ return 0;
+ },
useSyncScheduling: true,
});
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js
new file mode 100644
index 0000000000000..ec5d126974a67
--- /dev/null
+++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React;
+var ReactNoop;
+
+describe('ReactIncrementalRoot', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ });
+
+ function span(prop) {
+ return {type: 'span', children: [], prop};
+ }
+
+ it('prerenders roots', () => {
+ const root = ReactNoop.createRoot();
+ const work = root.prerender();
+ expect(root.getChildren()).toEqual([]);
+ work.commit();
+ expect(root.getChildren()).toEqual([span('A')]);
+ });
+
+ it('resolves `then` callback synchronously if tree is already completed', () => {
+ const root = ReactNoop.createRoot();
+ const work = root.prerender();
+ ReactNoop.flush();
+ let wasCalled = false;
+ work.then(() => {
+ wasCalled = true;
+ });
+ expect(wasCalled).toBe(true);
+ });
+
+ it('does not restart a completed tree if there were no additional updates', () => {
+ let ops = [];
+ function Foo(props) {
+ ops.push('Foo');
+ return ;
+ }
+ const root = ReactNoop.createRoot();
+ const work = root.prerender(Hi);
+
+ ReactNoop.flush();
+ expect(ops).toEqual(['Foo']);
+ expect(root.getChildren([]));
+
+ work.then(() => {
+ ops.push('Root completed');
+ work.commit();
+ ops.push('Root committed');
+ });
+
+ expect(ops).toEqual([
+ 'Foo',
+ 'Root completed',
+ // Should not re-render Foo
+ 'Root committed',
+ ]);
+ expect(root.getChildren([span('Hi')]));
+ });
+
+ it('works on a blocked tree if the expiration time is less than or equal to the blocked update', () => {
+ let ops = [];
+ function Foo(props) {
+ ops.push('Foo: ' + props.children);
+ return ;
+ }
+ const root = ReactNoop.createRoot();
+ root.prerender(A);
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Foo: A']);
+ expect(root.getChildren()).toEqual([]);
+
+ // workA and workB have the same expiration time
+ root.prerender(B);
+ ReactNoop.flush();
+
+ // Should have re-rendered the root, even though it's blocked
+ // from committing.
+ expect(ops).toEqual(['Foo: A', 'Foo: B']);
+ expect(root.getChildren()).toEqual([]);
+ });
+
+ it('does not work on on a blocked tree if the expiration time is greater than the blocked update', () => {
+ let ops = [];
+ function Foo(props) {
+ ops.push('Foo: ' + props.children);
+ return ;
+ }
+ const root = ReactNoop.createRoot();
+ root.prerender(A);
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Foo: A']);
+ expect(root.getChildren()).toEqual([]);
+
+ // workB has a later expiration time
+ ReactNoop.expire(1000);
+ root.prerender(B);
+ ReactNoop.flush();
+
+ // Should not have re-rendered the root at the later expiration time
+ expect(ops).toEqual(['Foo: A']);
+ expect(root.getChildren()).toEqual([]);
+ });
+
+ it('commits earlier work without committing later work', () => {
+ const root = ReactNoop.createRoot();
+ const work1 = root.prerender();
+ ReactNoop.flush();
+
+ expect(root.getChildren()).toEqual([]);
+
+ // Second prerender has a later expiration time
+ ReactNoop.expire(1000);
+ root.prerender();
+
+ work1.commit();
+
+ // Should not have re-rendered the root at the later expiration time
+ expect(root.getChildren()).toEqual([span('A')]);
+ });
+
+ it('flushes earlier work if later work is committed', () => {
+ let ops = [];
+ const root = ReactNoop.createRoot();
+ const work1 = root.prerender();
+ // Second prerender has a later expiration time
+ ReactNoop.expire(1000);
+ const work2 = root.prerender();
+
+ work1.then(() => ops.push('complete 1'));
+ work2.then(() => ops.push('complete 2'));
+
+ work2.commit();
+
+ // Because the later prerender was committed, the earlier one should have
+ // committed, too.
+ expect(root.getChildren()).toEqual([span('B')]);
+ expect(ops).toEqual(['complete 1', 'complete 2']);
+ });
+
+ it('committing work on one tree does not commit or expire work in a separate tree', () => {
+ let ops = [];
+
+ const rootA = ReactNoop.createRoot('A');
+ const rootB = ReactNoop.createRoot('B');
+
+ function Foo(props) {
+ ops.push(props.label);
+ return ;
+ }
+
+ // Prerender work on two separate roots
+ const workA = rootA.prerender();
+ rootB.prerender();
+
+ expect(rootA.getChildren()).toEqual([]);
+ expect(rootB.getChildren()).toEqual([]);
+ expect(ops).toEqual([]);
+
+ // Commit root A. This forces the remaining work on root A to expire, but
+ // should not expire work on root B.
+ workA.commit();
+
+ expect(rootA.getChildren()).toEqual([span('A')]);
+ expect(rootB.getChildren()).toEqual([]);
+ expect(ops).toEqual(['A']);
+ });
+});
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
index fdeefaa3d975a..d5cdd22ae431a 100644
--- a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
@@ -54,6 +54,14 @@ describe('ReactIncrementalTriangle', () => {
};
}
+ const EXPIRE = 'EXPIRE';
+ function expire(ms) {
+ return {
+ type: EXPIRE,
+ ms,
+ };
+ }
+
function TriangleSimulator() {
let triangles = [];
let leafTriangles = [];
@@ -212,6 +220,9 @@ describe('ReactIncrementalTriangle', () => {
targetTriangle.activate();
}
break;
+ case EXPIRE:
+ ReactNoop.expire(action.ms);
+ break;
default:
break;
}
@@ -251,7 +262,7 @@ describe('ReactIncrementalTriangle', () => {
}
function randomAction() {
- switch (randomInteger(0, 4)) {
+ switch (randomInteger(0, 5)) {
case 0:
return flush(randomInteger(0, totalTriangles * 1.5));
case 1:
@@ -260,6 +271,8 @@ describe('ReactIncrementalTriangle', () => {
return interrupt();
case 3:
return toggle(randomInteger(0, totalChildren));
+ case 4:
+ return expire(randomInteger(0, 1500));
default:
throw new Error('Switch statement should be exhaustive');
}
@@ -290,6 +303,9 @@ describe('ReactIncrementalTriangle', () => {
case TOGGLE:
result += `toggle(${action.childIndex})`;
break;
+ case EXPIRE:
+ result += `expire(${action.ms})`;
+ break;
default:
throw new Error('Switch statement should be exhaustive');
}
diff --git a/src/renderers/testing/ReactTestRendererFiberEntry.js b/src/renderers/testing/ReactTestRendererFiberEntry.js
index 6779c0dc3662d..2a6d9e1ea5763 100644
--- a/src/renderers/testing/ReactTestRendererFiberEntry.js
+++ b/src/renderers/testing/ReactTestRendererFiberEntry.js
@@ -247,6 +247,11 @@ var TestRenderer = ReactFiberReconciler({
useSyncScheduling: true,
getPublicInstance,
+
+ now(): number {
+ // Test renderer does not use expiration
+ return 0;
+ },
});
var defaultTestOptions = {