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 = {