diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index bd8e631b116..f1b2590ccf6 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -344,6 +344,7 @@ const bundles = [ label: 'native-cs-fiber', manglePropertiesOnProd: false, name: 'react-native-cs-renderer', + featureFlags: 'src/renderers/native-cs/ReactNativeCSFeatureFlags', paths: [ 'src/renderers/native-cs/**/*.js', 'src/renderers/shared/**/*.js', diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 5a9d7fc5871..63c33b2cf5f 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -25,28 +25,28 @@ "gzip": 6703 }, "react-dom.development.js (UMD_DEV)": { - "size": 621975, - "gzip": 143324 + "size": 629392, + "gzip": 144478 }, "react-dom.production.min.js (UMD_PROD)": { - "size": 100785, - "gzip": 31791 + "size": 102133, + "gzip": 32176 }, "react-dom.development.js (NODE_DEV)": { - "size": 584244, - "gzip": 134628 + "size": 591659, + "gzip": 135778 }, "react-dom.production.min.js (NODE_PROD)": { - "size": 105107, - "gzip": 33011 + "size": 106470, + "gzip": 33400 }, "ReactDOMFiber-dev.js (FB_DEV)": { - "size": 581133, - "gzip": 134017 + "size": 588553, + "gzip": 135174 }, "ReactDOMFiber-prod.js (FB_PROD)": { - "size": 412537, - "gzip": 92098 + "size": 419563, + "gzip": 93265 }, "react-dom-test-utils.development.js (NODE_DEV)": { "size": 41660, @@ -113,44 +113,44 @@ "gzip": 6214 }, "react-art.development.js (UMD_DEV)": { - "size": 368071, - "gzip": 81389 + "size": 375488, + "gzip": 82528 }, "react-art.production.min.js (UMD_PROD)": { - "size": 82726, - "gzip": 25620 + "size": 84095, + "gzip": 26126 }, "react-art.development.js (NODE_DEV)": { - "size": 292404, - "gzip": 62177 + "size": 299846, + "gzip": 63339 }, "react-art.production.min.js (NODE_PROD)": { - "size": 52050, - "gzip": 16367 + "size": 53383, + "gzip": 16723 }, "ReactARTFiber-dev.js (FB_DEV)": { - "size": 291266, - "gzip": 62192 + "size": 298713, + "gzip": 63361 }, "ReactARTFiber-prod.js (FB_PROD)": { - "size": 216787, - "gzip": 45002 + "size": 223840, + "gzip": 46164 }, "ReactNativeFiber-dev.js (RN_DEV)": { - "size": 278766, - "gzip": 48380 + "size": 284319, + "gzip": 49050 }, "ReactNativeFiber-prod.js (RN_PROD)": { - "size": 216964, - "gzip": 37578 + "size": 222241, + "gzip": 38248 }, "react-test-renderer.development.js (NODE_DEV)": { - "size": 296127, - "gzip": 62596 + "size": 303543, + "gzip": 63748 }, "ReactTestRendererFiber-dev.js (FB_DEV)": { - "size": 294979, - "gzip": 62607 + "size": 302400, + "gzip": 63758 }, "react-test-renderer-shallow.development.js (NODE_DEV)": { "size": 9215, @@ -161,8 +161,8 @@ "gzip": 2221 }, "react-noop-renderer.development.js (NODE_DEV)": { - "size": 283716, - "gzip": 59508 + "size": 292422, + "gzip": 60867 }, "react-dom-server.development.js (UMD_DEV)": { "size": 120897, @@ -189,16 +189,16 @@ "gzip": 7520 }, "ReactNativeRTFiber-dev.js (RN_DEV)": { - "size": 210676, - "gzip": 35845 + "size": 216229, + "gzip": 36510 }, "ReactNativeRTFiber-prod.js (RN_PROD)": { - "size": 158649, - "gzip": 26594 + "size": 163926, + "gzip": 27258 }, "react-test-renderer.production.min.js (NODE_PROD)": { - "size": 53594, - "gzip": 16631 + "size": 54939, + "gzip": 17016 }, "react-test-renderer-shallow.production.min.js (NODE_PROD)": { "size": 4536, @@ -209,20 +209,20 @@ "gzip": 4241 }, "react-reconciler.development.js (NODE_DEV)": { - "size": 271013, - "gzip": 56661 + "size": 278428, + "gzip": 57809 }, "react-reconciler.production.min.js (NODE_PROD)": { - "size": 37656, - "gzip": 11733 + "size": 38988, + "gzip": 12157 }, "ReactNativeCSFiber-dev.js (RN_DEV)": { - "size": 203064, - "gzip": 34086 + "size": 208604, + "gzip": 34749 }, "ReactNativeCSFiber-prod.js (RN_PROD)": { - "size": 153544, - "gzip": 25371 + "size": 158812, + "gzip": 26057 } } } \ No newline at end of file diff --git a/src/renderers/native-cs/ReactNativeCSFeatureFlags.js b/src/renderers/native-cs/ReactNativeCSFeatureFlags.js new file mode 100644 index 00000000000..874fdf32560 --- /dev/null +++ b/src/renderers/native-cs/ReactNativeCSFeatureFlags.js @@ -0,0 +1,24 @@ +/** + * 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 ReactNativeCSFeatureFlags + * @flow + */ + +'use strict'; + +import type {FeatureFlags} from 'ReactFeatureFlags'; + +var ReactNativeCSFeatureFlags: FeatureFlags = { + enableAsyncSubtreeAPI: true, + enableAsyncSchedulingByDefaultInReactDOM: false, + // React Native CS uses persistent reconciler. + enableMutatingReconciler: false, + enableNoopReconciler: false, + enablePersistentReconciler: true, +}; + +module.exports = ReactNativeCSFeatureFlags; diff --git a/src/renderers/native-cs/ReactNativeCSFiberEntry.js b/src/renderers/native-cs/ReactNativeCSFiberEntry.js index 66c7cdc0adb..4fe83bd8577 100644 --- a/src/renderers/native-cs/ReactNativeCSFiberEntry.js +++ b/src/renderers/native-cs/ReactNativeCSFiberEntry.js @@ -154,34 +154,32 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ persistence: { cloneInstance( instance: Instance, - updatePayload: Object, + updatePayload: null | Object, type: string, oldProps: Props, newProps: Props, internalInstanceHandle: Object, keepChildren: boolean, + recyclableInstance: null | Instance, ): Instance { return 0; }, - tryToReuseInstance( - instance: Instance, - updatePayload: Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - ): Instance { + + cloneContainer( + container: Container, + recyclableContainer: Container, + ): Container { return 0; }, - createRootInstance( - rootContainerInstance: Container, - hostContext: {}, - ): Instance { - return 123; - }, - commitRootInstance(rootInstance: Instance): void {}, + appendInititalChildToContainer( + container: Container, + child: Instance | TextInstance, + ): void {}, + + finalizeContainerChildren(container: Container): void {}, + + replaceContainer(oldContainer: Container, newContainer: Container): void {}, }, }); diff --git a/src/renderers/native-cs/__tests__/ReactNativeCS-test.js b/src/renderers/native-cs/__tests__/ReactNativeCS-test.js index f04074529b0..c2602f856f4 100644 --- a/src/renderers/native-cs/__tests__/ReactNativeCS-test.js +++ b/src/renderers/native-cs/__tests__/ReactNativeCS-test.js @@ -12,6 +12,8 @@ var React; var ReactNativeCS; +jest.mock('ReactFeatureFlags', () => require('ReactNativeCSFeatureFlags')); + describe('ReactNativeCS', () => { beforeEach(() => { jest.resetModules(); diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 89bfc33c2d7..7e67fcccb25 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -85,7 +85,7 @@ function removeChild( let elapsedTimeInMs = 0; -var NoopRenderer = ReactFiberReconciler({ +var SharedHostConfig = { getRootHostContext() { if (failInBeginPhase) { throw new Error('Error in host config.'); @@ -176,43 +176,111 @@ var NoopRenderer = ReactFiberReconciler({ now(): number { return elapsedTimeInMs; }, +}; + +var MutationHostConfig = { + commitMount(instance: Instance, type: string, newProps: Props): void { + // Noop + }, + + commitUpdate( + instance: Instance, + updatePayload: Object, + type: string, + oldProps: Props, + newProps: Props, + ): void { + instance.prop = newProps.prop; + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.text = newText; + }, + + appendChild: appendChild, + appendChildToContainer: appendChild, + insertBefore: insertBefore, + insertInContainerBefore: insertBefore, + removeChild: removeChild, + removeChildFromContainer: removeChild, - mutation: { - commitMount(instance: Instance, type: string, newProps: Props): void { - // Noop - }, - - commitUpdate( - instance: Instance, - updatePayload: Object, - type: string, - oldProps: Props, - newProps: Props, - ): void { - instance.prop = newProps.prop; - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - - resetTextContent(instance: Instance): void {}, - }, -}); + resetTextContent(instance: Instance): void {}, +}; + +var PersistenceHostConfig = { + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const clone = { + id: instance.id, + type: type, + children: keepChildren ? instance.children : [], + prop: newProps.prop, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + return clone; + }, + + cloneContainer( + container: Container, + recyclableContainer: Container, + ): Container { + return {rootID: container.rootID, children: []}; + }, + + appendInititalChildToContainer( + parentInstance: Container, + child: Instance | TextInstance, + ) { + parentInstance.children.push(child); + }, + + finalizeContainerChildren(container: Container): void {}, + + replaceContainer(oldContainer: Container, newContainer: Container): void { + rootContainers.set(oldContainer.rootID, newContainer); + }, +}; + +// They are created lazily because only one can be created per test file. +var renderer = null; +var persistentRenderer = null; +function getRenderer() { + return ( + renderer || + (renderer = ReactFiberReconciler({ + ...SharedHostConfig, + mutation: MutationHostConfig, + })) + ); +} +function getPersistentRenderer() { + return ( + persistentRenderer || + (persistentRenderer = ReactFiberReconciler({ + ...SharedHostConfig, + persistence: PersistenceHostConfig, + })) + ); +} var rootContainers = new Map(); var roots = new Map(); +var persistentRoots = new Map(); var DEFAULT_ROOT_ID = ''; let yieldedValues = null; @@ -265,6 +333,7 @@ var ReactNoop = { rootID: string, callback: ?Function, ) { + const NoopRenderer = getRenderer(); let root = roots.get(rootID); if (!root) { const container = {rootID: rootID, children: []}; @@ -275,7 +344,24 @@ var ReactNoop = { NoopRenderer.updateContainer(element, root, null, callback); }, + renderToPersistentRootWithID( + element: React$Element, + rootID: string, + callback: ?Function, + ) { + const PersistentNoopRenderer = getPersistentRenderer(); + let root = persistentRoots.get(rootID); + if (!root) { + const container = {rootID: rootID, children: []}; + rootContainers.set(rootID, container); + root = PersistentNoopRenderer.createContainer(container, false); + persistentRoots.set(rootID, root); + } + PersistentNoopRenderer.updateContainer(element, root, null, callback); + }, + unmountRootWithID(rootID: string) { + const NoopRenderer = getRenderer(); const root = roots.get(rootID); if (root) { NoopRenderer.updateContainer(null, root, null, () => { @@ -288,6 +374,7 @@ var ReactNoop = { findInstance( componentOrElement: Element | ?React$Component, ): null | Instance | TextInstance { + const NoopRenderer = getRenderer(); if (componentOrElement == null) { return null; } @@ -364,13 +451,25 @@ var ReactNoop = { return !!scheduledCallback; }, - batchedUpdates: NoopRenderer.batchedUpdates, + batchedUpdates(...args: Array) { + const NoopRenderer = getRenderer(); + return NoopRenderer.batchedUpdates(...args); + }, - deferredUpdates: NoopRenderer.deferredUpdates, + deferredUpdates(...args: Array) { + const NoopRenderer = getRenderer(); + return NoopRenderer.deferredUpdates(...args); + }, - unbatchedUpdates: NoopRenderer.unbatchedUpdates, + unbatchedUpdates(...args: Array) { + const NoopRenderer = getRenderer(); + return NoopRenderer.unbatchedUpdates(...args); + }, - flushSync: NoopRenderer.flushSync, + flushSync(...args: Array) { + const NoopRenderer = getRenderer(); + return NoopRenderer.flushSync(...args); + }, // Logs the current state of the tree. dumpTree(rootID: string = DEFAULT_ROOT_ID) { diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 935b897b3ca..b31a28d3a2e 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -458,7 +458,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if ( current === null || current.tag !== HostPortal || - current.stateNode.containerInfo !== portal.containerInfo || + // Persistent containers don't have permanent identity. + // current.stateNode.containerInfo !== portal.containerInfo || current.stateNode.implementation !== portal.implementation ) { // Insert @@ -1315,7 +1316,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if ( child.tag === HostPortal && - child.stateNode.containerInfo === portal.containerInfo && + // Persistent containers don't have permanent identity. + // child.stateNode.containerInfo === portal.containerInfo && child.stateNode.implementation === portal.implementation ) { deleteRemainingChildren(returnFiber, child.sibling); diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 633911db341..6c08388990a 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -445,6 +445,7 @@ exports.createFiberFromPortal = function( fiber.expirationTime = expirationTime; fiber.stateNode = { containerInfo: portal.containerInfo, + pendingContainerInfo: portal.containerInfo, // Used by persistent updates implementation: portal.implementation, }; return fiber; diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 1cf90d93e30..7a9b8d96a9b 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -13,6 +13,7 @@ import type {Fiber} from 'ReactFiber'; import type {HostConfig} from 'ReactFiberReconciler'; +var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { ClassComponent, @@ -42,7 +43,7 @@ module.exports = function( config: HostConfig, captureError: (failedFiber: Fiber, error: mixed) => Fiber | null, ) { - const {getPublicInstance} = config; + const {getPublicInstance, mutation, persistence} = config; if (__DEV__) { var callComponentWillUnmountWithTimerInDev = function(current, instance) { @@ -162,6 +163,10 @@ module.exports = function( // We have no life-cycles associated with text. return; } + case HostPortal: { + // We have no life-cycles associated with portals. + return; + } default: { invariant( false, @@ -222,7 +227,14 @@ module.exports = function( // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. - unmountHostComponents(current); + if (ReactFeatureFlags.enableMutatingReconciler && mutation) { + unmountHostComponents(current); + } else if ( + ReactFeatureFlags.enablePersistentReconciler && + persistence + ) { + emptyPortalContainer(current); + } return; } } @@ -239,7 +251,11 @@ module.exports = function( commitUnmount(node); // Visit children because they may contain more composite or host nodes. // Skip portals because commitUnmount() currently visits them recursively. - if (node.child !== null && node.tag !== HostPortal) { + if ( + node.child !== null && + // Drill down into portals only if we use mutation since that branch is recursive + (!mutation || node.tag !== HostPortal) + ) { node.child.return = node; node = node.child; continue; @@ -272,22 +288,82 @@ module.exports = function( } } - if (!config.mutation) { - return { - commitResetTextContent(finishedWork: Fiber) {}, - commitPlacement(finishedWork: Fiber) {}, - commitDeletion(current: Fiber) { - // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(current); - detachFiber(current); - }, - commitWork(current: Fiber | null, finishedWork: Fiber) {}, - commitLifeCycles, - commitAttachRef, - commitDetachRef, - }; + if (!mutation) { + let commitContainer; + if (persistence) { + const {replaceContainer, cloneContainer} = persistence; + var emptyPortalContainer = function(current: Fiber) { + const portal: {containerInfo: C, pendingContainerInfo: C} = + current.stateNode; + const {containerInfo, pendingContainerInfo} = portal; + const emptyContainer = cloneContainer( + containerInfo, + pendingContainerInfo, + ); + replaceContainer(containerInfo, emptyContainer); + }; + commitContainer = function(finishedWork: Fiber) { + switch (finishedWork.tag) { + case ClassComponent: { + return; + } + case HostComponent: { + return; + } + case HostText: { + return; + } + case HostRoot: + case HostPortal: { + const portalOrRoot: {containerInfo: C, pendingContainerInfo: C} = + finishedWork.stateNode; + const {containerInfo, pendingContainerInfo} = portalOrRoot; + replaceContainer(containerInfo, pendingContainerInfo); + // Swap out the current container. + portalOrRoot.containerInfo = pendingContainerInfo; + // The old one is now free to be recycled. + portalOrRoot.pendingContainerInfo = containerInfo; + return; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + } + } + }; + } else { + commitContainer = function(finishedWork: Fiber) { + // Noop + }; + } + if ( + ReactFeatureFlags.enablePersistentReconciler || + ReactFeatureFlags.enableNoopReconciler + ) { + return { + commitResetTextContent(finishedWork: Fiber) {}, + commitPlacement(finishedWork: Fiber) {}, + commitDeletion(current: Fiber) { + // Detach refs and call componentWillUnmount() on the whole subtree. + commitNestedUnmounts(current); + detachFiber(current); + }, + commitWork(current: Fiber | null, finishedWork: Fiber) { + commitContainer(finishedWork); + }, + commitLifeCycles, + commitAttachRef, + commitDetachRef, + }; + } else if (persistence) { + invariant(false, 'Persistent reconciler is disabled.'); + } else { + invariant(false, 'Noop reconciler is disabled.'); + } } - const { commitMount, commitUpdate, @@ -299,7 +375,7 @@ module.exports = function( insertInContainerBefore, removeChild, removeChildFromContainer, - } = config.mutation; + } = mutation; function getHostParentFiber(fiber: Fiber): Fiber { let parent = fiber.return; @@ -599,13 +675,17 @@ module.exports = function( resetTextContent(current.stateNode); } - return { - commitResetTextContent, - commitPlacement, - commitDeletion, - commitWork, - commitLifeCycles, - commitAttachRef, - commitDetachRef, - }; + if (ReactFeatureFlags.enableMutatingReconciler) { + return { + commitResetTextContent, + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + commitAttachRef, + commitDetachRef, + }; + } else { + invariant(false, 'Mutating reconciler is disabled.'); + } }; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index afc3b5d52b8..b39d8594d5f 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -23,6 +23,7 @@ var { popContextProvider, popTopLevelContextObject, } = require('ReactFiberContext'); +var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var ReactFiberExpirationTime = require('ReactFiberExpirationTime'); @@ -55,6 +56,8 @@ module.exports = function( appendInitialChild, finalizeInitialChildren, prepareUpdate, + mutation, + persistence, } = config; const { @@ -161,6 +164,7 @@ module.exports = function( // down its children. Instead, we'll get insertions from each child in // the portal directly. } else if (node.child !== null) { + node.child.return = node; node = node.child; continue; } @@ -173,10 +177,225 @@ module.exports = function( } node = node.return; } + node.sibling.return = node.return; node = node.sibling; } } + let updateHostContainer; + let updateHostComponent; + let updateHostText; + if (mutation) { + if (ReactFeatureFlags.enableMutatingReconciler) { + // Mutation mode + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | PL, + type: T, + oldProps: P, + newProps: P, + rootContainerInstance: C, + ) { + // TODO: Type this specific to this type of component. + workInProgress.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. All the work is done in commitWork. + if (updatePayload) { + markUpdate(workInProgress); + } + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + // If the text differs, mark it as an update. All the work in done in commitWork. + if (oldText !== newText) { + markUpdate(workInProgress); + } + }; + } else { + invariant(false, 'Mutating reconciler is disabled.'); + } + } else if (persistence) { + if (ReactFeatureFlags.enablePersistentReconciler) { + // Persistent host tree mode + const { + cloneInstance, + cloneContainer, + appendInititalChildToContainer, + finalizeContainerChildren, + } = persistence; + + // An unfortunate fork of appendAllChildren because we have two different parent types. + const appendAllChildrenToContainer = function( + container: C, + workInProgress: Fiber, + ) { + // We only have the top Fiber that was created but we need recurse down its + // children to find all the terminal nodes. + let node = workInProgress.child; + while (node !== null) { + if (node.tag === HostComponent || node.tag === HostText) { + appendInititalChildToContainer(container, node.stateNode); + } else if (node.tag === HostPortal) { + // If we have a portal child, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === workInProgress) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === workInProgress) { + return; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + } + }; + updateHostContainer = function(workInProgress: Fiber) { + const portalOrRoot: {containerInfo: C, pendingContainerInfo: C} = + workInProgress.stateNode; + const currentContainer = portalOrRoot.containerInfo; + const recyclableContainer = portalOrRoot.pendingContainerInfo; + + const childrenUnchanged = workInProgress.firstEffect === null; + if (childrenUnchanged) { + // No changes, just reuse the existing instance. + // Note that this might release a previous clone. + portalOrRoot.pendingContainerInfo = currentContainer; + } else { + let newContainer = cloneContainer( + currentContainer, + recyclableContainer, + ); + if (finalizeContainerChildren(newContainer)) { + markUpdate(workInProgress); + } + portalOrRoot.pendingContainerInfo = newContainer; + // If children might have changed, we have to add them all to the set. + appendAllChildrenToContainer(newContainer, workInProgress); + // Schedule an update on the container to swap out the container. + markUpdate(workInProgress); + } + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | PL, + type: T, + oldProps: P, + newProps: P, + rootContainerInstance: C, + ) { + // If there are no effects associated with this node, then none of our children had any updates. + // This guarantees that we can reuse all of them. + const childrenUnchanged = workInProgress.firstEffect === null; + const currentInstance = current.stateNode; + if (childrenUnchanged && updatePayload === null) { + // No changes, just reuse the existing instance. + // Note that this might release a previous clone. + workInProgress.stateNode = currentInstance; + } else { + let recyclableInstance = workInProgress.stateNode; + let newInstance = cloneInstance( + currentInstance, + updatePayload, + type, + oldProps, + newProps, + workInProgress, + childrenUnchanged, + recyclableInstance, + ); + if ( + finalizeInitialChildren( + newInstance, + type, + newProps, + rootContainerInstance, + ) + ) { + markUpdate(workInProgress); + } + workInProgress.stateNode = newInstance; + if (childrenUnchanged) { + // If there are no other effects in this tree, we need to flag this node as having one. + // Even though we're not going to use it for anything. + // Otherwise parents won't know that there are new children to propagate upwards. + markUpdate(workInProgress); + } else { + // If children might have changed, we have to add them all to the set. + appendAllChildren(newInstance, workInProgress); + } + } + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + if (oldText !== newText) { + // If the text content differs, we'll create a new text instance for it. + const rootContainerInstance = getRootHostContainer(); + const currentHostContext = getHostContext(); + workInProgress.stateNode = createTextInstance( + newText, + rootContainerInstance, + currentHostContext, + workInProgress, + ); + // We'll have to mark it as having an effect, even though we won't use the effect for anything. + // This lets the parents know that at least one of their children has changed. + markUpdate(workInProgress); + } + }; + } else { + invariant(false, 'Persistent reconciler is disabled.'); + } + } else { + if (ReactFeatureFlags.enableNoopReconciler) { + // No host operations + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | PL, + type: T, + oldProps: P, + newProps: P, + rootContainerInstance: C, + ) { + // Noop + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + // Noop + }; + } else { + invariant(false, 'Noop reconciler is disabled.'); + } + } + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -219,6 +438,7 @@ module.exports = function( // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } + updateHostContainer(workInProgress); return null; } case HostComponent: { @@ -244,13 +464,16 @@ module.exports = function( currentHostContext, ); - // TODO: Type this specific to this type of component. - workInProgress.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload) { - markUpdate(workInProgress); - } + updateHostComponent( + current, + workInProgress, + updatePayload, + type, + oldProps, + newProps, + rootContainerInstance, + ); + if (current.ref !== workInProgress.ref) { markRef(workInProgress); } @@ -325,9 +548,7 @@ module.exports = function( const oldText = current.memoizedProps; // If we have an alternate, that means this is an update and we need // to schedule a side-effect to do the updates. - if (oldText !== newText) { - markUpdate(workInProgress); - } + updateHostText(current, workInProgress, oldText, newText); } else { if (typeof newText !== 'string') { invariant( @@ -373,6 +594,7 @@ module.exports = function( return null; case HostPortal: popHostContainer(workInProgress); + updateHostContainer(workInProgress); return null; // Error cases case IndeterminateComponent: diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 7f6a7c3017c..2fcff4aaa7c 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -100,7 +100,7 @@ export type HostConfig = { +hydration?: HydrationHostConfig, +mutation?: MutableUpdatesHostConfig, - +persistence?: PersistentUpdatesHostConfig, + +persistence?: PersistentUpdatesHostConfig, }; type MutableUpdatesHostConfig = { @@ -132,28 +132,24 @@ type MutableUpdatesHostConfig = { removeChildFromContainer(container: C, child: I | TI): void, }; -type PersistentUpdatesHostConfig = { +type PersistentUpdatesHostConfig = { cloneInstance( instance: I, - updatePayload: PL, - type: T, - oldProps: P, - newProps: P, - internalInstanceHandle: OpaqueHandle, - keepChildren: boolean, - ): I, - tryToReuseInstance( - instance: I, - updatePayload: PL, + updatePayload: null | PL, type: T, oldProps: P, newProps: P, internalInstanceHandle: OpaqueHandle, keepChildren: boolean, + recyclableInstance: I, ): I, - createRootInstance(rootContainerInstance: C, hostContext: CX): I, - commitRootInstance(rootInstance: I): void, + cloneContainer(container: C, recyclableContainer: C): C, + + appendInititalChildToContainer(container: C, child: I | TI): void, + finalizeContainerChildren(container: C): void, + + replaceContainer(oldContainer: C, newContainer: C): void, }; type HydrationHostConfig = { diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 419e842e08b..704cd60b928 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -17,6 +17,8 @@ const {createHostRootFiber} = require('ReactFiber'); export type FiberRoot = { // Any additional information from the host associated with this root. containerInfo: any, + // Used only by persistent updates. + pendingContainerInfo: any, // The currently active root fiber. This is the mutable root of the tree. current: Fiber, // Determines if this root has already been added to the schedule for work. @@ -40,6 +42,7 @@ exports.createFiberRoot = function( const root = { current: uninitializedFiber, containerInfo: containerInfo, + pendingContainerInfo: containerInfo, isScheduled: false, nextScheduledRoot: null, context: null, diff --git a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js new file mode 100644 index 00000000000..bd2043c7ee3 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -0,0 +1,207 @@ +/** + * 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; +let ReactPortal; + +describe('ReactPersistent', () => { + beforeEach(() => { + jest.resetModules(); + + const ReactFeatureFlags = require('ReactFeatureFlags'); + ReactFeatureFlags.enableMutableReconciler = false; + ReactFeatureFlags.enablePersistentReconciler = true; + ReactFeatureFlags.enableNoopReconciler = false; + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + ReactPortal = require('ReactPortal'); + }); + + const DEFAULT_ROOT_ID = 'persistent-test'; + + function render(element) { + ReactNoop.renderToPersistentRootWithID(element, DEFAULT_ROOT_ID); + } + + function div(...children) { + children = children.map(c => (typeof c === 'string' ? {text: c} : c)); + return {type: 'div', children, prop: undefined}; + } + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + function getChildren() { + return ReactNoop.getChildren(DEFAULT_ROOT_ID); + } + + it('can update child nodes of a host instance', () => { + function Bar(props) { + return {props.text}; + } + + function Foo(props) { + return ( +
+ + {props.text === 'World' ? : null} +
+ ); + } + + render(); + ReactNoop.flush(); + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div(span())]); + + render(); + ReactNoop.flush(); + var newChildren = getChildren(); + expect(newChildren).toEqual([div(span(), span())]); + + expect(originalChildren).toEqual([div(span())]); + }); + + it('can reuse child nodes between updates', () => { + function Baz(props) { + return ; + } + class Bar extends React.Component { + shouldComponentUpdate(newProps) { + return false; + } + render() { + return ; + } + } + function Foo(props) { + return ( +
+ + {props.text === 'World' ? : null} +
+ ); + } + + render(); + ReactNoop.flush(); + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div(span('Hello'))]); + + render(); + ReactNoop.flush(); + var newChildren = getChildren(); + expect(newChildren).toEqual([div(span('Hello'), span('World'))]); + + expect(originalChildren).toEqual([div(span('Hello'))]); + + // Reused node should have reference equality + expect(newChildren[0].children[0]).toBe(originalChildren[0].children[0]); + }); + + it('can update child text nodes', () => { + function Foo(props) { + return ( +
+ {props.text} + +
+ ); + } + + render(); + ReactNoop.flush(); + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div('Hello', span())]); + + render(); + ReactNoop.flush(); + var newChildren = getChildren(); + expect(newChildren).toEqual([div('World', span())]); + + expect(originalChildren).toEqual([div('Hello', span())]); + }); + + it('supports portals', () => { + function Parent(props) { + return
{props.children}
; + } + + function BailoutSpan() { + return ; + } + + class BailoutTest extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return ; + } + } + + function Child(props) { + return
{props.children}
; + } + const portalID = 'persistent-portal-test'; + const portalContainer = {rootID: portalID, children: []}; + render( + + {ReactPortal.createPortal(, portalContainer, null)} + , + ); + ReactNoop.flush(); + + expect(portalContainer.children).toEqual([]); + + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div()]); + var originalPortalChildren = ReactNoop.getChildren(portalID); + expect(originalPortalChildren).toEqual([div(span())]); + + render( + + {ReactPortal.createPortal( + Hello {'World'}, + portalContainer, + null, + )} + , + ); + ReactNoop.flush(); + + var newChildren = getChildren(); + expect(newChildren).toEqual([div()]); + var newPortalChildren = ReactNoop.getChildren(portalID); + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]); + + expect(originalChildren).toEqual([div()]); + expect(originalPortalChildren).toEqual([div(span())]); + + // Reused portal children should have reference equality + expect(newPortalChildren[0].children[0]).toBe( + originalPortalChildren[0].children[0], + ); + + // Deleting the Portal, should clear its children + render(); + ReactNoop.flush(); + + var clearedPortalChildren = ReactNoop.getChildren(portalID); + expect(clearedPortalChildren).toEqual([]); + + // The original is unchanged. + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]); + }); +}); diff --git a/src/renderers/shared/utils/ReactFeatureFlags.js b/src/renderers/shared/utils/ReactFeatureFlags.js index d60ef12f551..6291d1054ab 100644 --- a/src/renderers/shared/utils/ReactFeatureFlags.js +++ b/src/renderers/shared/utils/ReactFeatureFlags.js @@ -13,11 +13,20 @@ export type FeatureFlags = {| enableAsyncSubtreeAPI: boolean, enableAsyncSchedulingByDefaultInReactDOM: boolean, + enableMutatingReconciler: boolean, + enableNoopReconciler: boolean, + enablePersistentReconciler: boolean, |}; var ReactFeatureFlags: FeatureFlags = { enableAsyncSubtreeAPI: true, enableAsyncSchedulingByDefaultInReactDOM: false, + // Mutating mode (React DOM, React ART, React Native): + enableMutatingReconciler: true, + // Experimental noop mode (currently unused): + enableNoopReconciler: false, + // Experimental persistent mode (CS): + enablePersistentReconciler: false, }; if (__DEV__) {