From 24eebf2fc741fb7b8266ac3b69360a55b376b5fc Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 00:07:56 -0700 Subject: [PATCH 01/16] [CS] Clone container instead of new root concept The extra "root" concept is kind of unnecessary. Instead of having a mutable container even in the persistent mode, I'll instead make the container be immutable too and be cloned. Then the "commit" just becomes swapping the previous container for the new one. --- .../native-cs/ReactNativeCSFiberEntry.js | 24 ++++++++++++++----- .../shared/fiber/ReactFiberReconciler.js | 12 ++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/renderers/native-cs/ReactNativeCSFiberEntry.js b/src/renderers/native-cs/ReactNativeCSFiberEntry.js index 66c7cdc0adb..f05f9399660 100644 --- a/src/renderers/native-cs/ReactNativeCSFiberEntry.js +++ b/src/renderers/native-cs/ReactNativeCSFiberEntry.js @@ -175,13 +175,25 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ return 0; }, - createRootInstance( - rootContainerInstance: Container, - hostContext: {}, - ): Instance { - return 123; + cloneContainer(container: Container, keepChildren: boolean): Container { + return 0; }, - commitRootInstance(rootInstance: Instance): void {}, + tryToReuseContainer( + container: Container, + keepChildren: boolean, + ): Container { + return 0; + }, + + appendInititalChildToContainer( + container: Container, + child: Instance | TextInstance, + ): void {}, + + completeContainer( + oldContainer: Container, + newContainer: Container, + ): void {}, }, }); diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 7f6a7c3017c..3837bdcab23 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,7 +132,7 @@ type MutableUpdatesHostConfig = { removeChildFromContainer(container: C, child: I | TI): void, }; -type PersistentUpdatesHostConfig = { +type PersistentUpdatesHostConfig = { cloneInstance( instance: I, updatePayload: PL, @@ -152,8 +152,12 @@ type PersistentUpdatesHostConfig = { keepChildren: boolean, ): I, - createRootInstance(rootContainerInstance: C, hostContext: CX): I, - commitRootInstance(rootInstance: I): void, + cloneContainer(container: C, keepChildren: boolean): C, + tryToReuseContainer(container: C, keepChildren: boolean): C, + + appendInititalChildToContainer(container: C, child: I | TI): void, + + completeContainer(oldContainer: C, newContainer: C): void, }; type HydrationHostConfig = { From 4beb04c3fd79a838dca2edce1305ed0510b1f779 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 11:10:07 -0700 Subject: [PATCH 02/16] Change the signature or persistence again We may need to clone without any updates, e.g. when the children are changed. Passing in the previous node is not enough to recycle since it won't have the up-to-date props and children. It's really only useful to for allocation pooling. --- src/renderers/native-cs/ReactNativeCSFiberEntry.js | 10 ++++++---- src/renderers/shared/fiber/ReactFiberReconciler.js | 13 +++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/renderers/native-cs/ReactNativeCSFiberEntry.js b/src/renderers/native-cs/ReactNativeCSFiberEntry.js index f05f9399660..37f6b215d06 100644 --- a/src/renderers/native-cs/ReactNativeCSFiberEntry.js +++ b/src/renderers/native-cs/ReactNativeCSFiberEntry.js @@ -154,7 +154,7 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ persistence: { cloneInstance( instance: Instance, - updatePayload: Object, + updatePayload: null | Object, type: string, oldProps: Props, newProps: Props, @@ -163,14 +163,15 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ ): Instance { return 0; }, - tryToReuseInstance( + cloneInstanceOrRecycle( instance: Instance, - updatePayload: Object, + updatePayload: null | Object, type: string, oldProps: Props, newProps: Props, internalInstanceHandle: Object, keepChildren: boolean, + recyclableInstance: null | Instance, ): Instance { return 0; }, @@ -178,9 +179,10 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ cloneContainer(container: Container, keepChildren: boolean): Container { return 0; }, - tryToReuseContainer( + cloneContainerOrRecycle( container: Container, keepChildren: boolean, + recyclableContainer: Container, ): Container { return 0; }, diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 3837bdcab23..ca17e866d74 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -135,25 +135,30 @@ type MutableUpdatesHostConfig = { type PersistentUpdatesHostConfig = { cloneInstance( instance: I, - updatePayload: PL, + updatePayload: null | PL, type: T, oldProps: P, newProps: P, internalInstanceHandle: OpaqueHandle, keepChildren: boolean, ): I, - tryToReuseInstance( + cloneInstanceOrRecycle( instance: I, - updatePayload: PL, + updatePayload: null | PL, type: T, oldProps: P, newProps: P, internalInstanceHandle: OpaqueHandle, keepChildren: boolean, + recyclableInstance: I, ): I, cloneContainer(container: C, keepChildren: boolean): C, - tryToReuseContainer(container: C, keepChildren: boolean): C, + cloneContainerOrRecycle( + container: C, + keepChildren: boolean, + recyclableContainer: C, + ): C, appendInititalChildToContainer(container: C, child: I | TI): void, From 7098f89904d2da870288406dc47c75f2b0c849e7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 11:12:43 -0700 Subject: [PATCH 03/16] Implement persistent updates This forks the update path for host fibers. For mutation mode we mark them as having an effect. For persistence mode, we clone the stateNode with new props/children. Next I'll do HostRoot and HostPortal. --- .../shared/fiber/ReactFiberCompleteWork.js | 174 +++++++++++++++++- 1 file changed, 164 insertions(+), 10 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index afc3b5d52b8..43756105dbe 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -55,6 +55,8 @@ module.exports = function( appendInitialChild, finalizeInitialChildren, prepareUpdate, + mutation, + persistence, } = config; const { @@ -177,6 +179,157 @@ module.exports = function( } } + let updateHostComponent; + let updateHostText; + if (mutation) { + // Mutation mode + 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 if (persistence) { + // Persistent host tree mode + const { + cloneInstance, + cloneInstanceOrRecycle, + cloneContainer, + cloneContainerOrRecycle, + appendInititalChildToContainer, + completeContainer, + } = persistence; + 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; + if (currentInstance === recyclableInstance) { + // We can't recycle the current, we'll need to clone. + newInstance = cloneInstance( + currentInstance, + updatePayload, + type, + oldProps, + newProps, + workInProgress, + childrenUnchanged, + ); + } else { + newInstance = cloneInstanceOrRecycle( + 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 { + // No host operations + 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 + }; + } + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -244,13 +397,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 +481,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( From 1c4118854287a4b3aced1e9b38b5670f34f0b2df Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 13:57:03 -0700 Subject: [PATCH 04/16] Refine protocol into a complete and commit phase finalizeContainerChildren will get called at the complete phase. replaceContainer will get called at commit. Also, drop the keepChildren flag. We'll never keep children as we'll never update a container if none of the children has changed. --- src/renderers/native-cs/ReactNativeCSFiberEntry.js | 10 ++++------ src/renderers/shared/fiber/ReactFiberCompleteWork.js | 2 +- src/renderers/shared/fiber/ReactFiberReconciler.js | 11 ++++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/renderers/native-cs/ReactNativeCSFiberEntry.js b/src/renderers/native-cs/ReactNativeCSFiberEntry.js index 37f6b215d06..1b61bfa7889 100644 --- a/src/renderers/native-cs/ReactNativeCSFiberEntry.js +++ b/src/renderers/native-cs/ReactNativeCSFiberEntry.js @@ -176,12 +176,11 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ return 0; }, - cloneContainer(container: Container, keepChildren: boolean): Container { + cloneContainer(container: Container): Container { return 0; }, cloneContainerOrRecycle( container: Container, - keepChildren: boolean, recyclableContainer: Container, ): Container { return 0; @@ -192,10 +191,9 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ child: Instance | TextInstance, ): void {}, - completeContainer( - oldContainer: Container, - newContainer: Container, - ): void {}, + finalizeContainerChildren(container: Container): void {}, + + replaceContainer(oldContainer: Container, newContainer: Container): void {}, }, }); diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 43756105dbe..96d39aa9563 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -219,7 +219,7 @@ module.exports = function( cloneContainer, cloneContainerOrRecycle, appendInititalChildToContainer, - completeContainer, + finalizeContainerChildren, } = persistence; updateHostComponent = function( current: Fiber, diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index ca17e866d74..a4cb0502c88 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -153,16 +153,13 @@ type PersistentUpdatesHostConfig = { recyclableInstance: I, ): I, - cloneContainer(container: C, keepChildren: boolean): C, - cloneContainerOrRecycle( - container: C, - keepChildren: boolean, - recyclableContainer: C, - ): C, + cloneContainer(container: C): C, + cloneContainerOrRecycle(container: C, recyclableContainer: C): C, appendInititalChildToContainer(container: C, child: I | TI): void, + finalizeContainerChildren(container: C): void, - completeContainer(oldContainer: C, newContainer: C): void, + replaceContainer(oldContainer: C, newContainer: C): void, }; type HydrationHostConfig = { From 787d359654c6c29b3e3d162c1d0c3442c9629e2d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 14:37:44 -0700 Subject: [PATCH 05/16] Implement persistent updates of roots and portals These are both "containers". Normally we rely on placement/deletion effects to deal with insertions into the containers. In the persistent mode we need to clone the container and append all the changed children to it. I needed somewhere to store these new containers before they're committed so I added another field. --- src/renderers/shared/fiber/ReactFiber.js | 1 + .../shared/fiber/ReactFiberCompleteWork.js | 73 +++++++++++++++++++ src/renderers/shared/fiber/ReactFiberRoot.js | 3 + 3 files changed, 77 insertions(+) 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/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 96d39aa9563..a5d1c658de8 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -179,10 +179,14 @@ module.exports = function( } } + let updateHostContainer; let updateHostComponent; let updateHostText; if (mutation) { // Mutation mode + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; updateHostComponent = function( current: Fiber, workInProgress: Fiber, @@ -221,6 +225,70 @@ module.exports = function( 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 = node.child; + continue; + } + if (node === workInProgress) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === workInProgress) { + return; + } + node = 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; + if (currentContainer === recyclableContainer) { + // We can't recycle the current, we'll need to clone. + newContainer = cloneContainer(currentContainer); + } else { + newContainer = cloneContainerOrRecycle( + 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, @@ -309,6 +377,9 @@ module.exports = function( }; } else { // No host operations + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; updateHostComponent = function( current: Fiber, workInProgress: Fiber, @@ -372,6 +443,7 @@ module.exports = function( // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } + updateHostContainer(workInProgress); return null; } case HostComponent: { @@ -527,6 +599,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/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, From 6c8e148d3ae0171435000b53be6f9be68b4d0be7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 14:58:01 -0700 Subject: [PATCH 06/16] Commit persistent work at the end by swapping out the container --- .../shared/fiber/ReactFiberCommitWork.js | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 1cf90d93e30..4afbc2ee425 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -273,6 +273,43 @@ module.exports = function( } if (!config.mutation) { + let commitContainer; + if (!config.persistence) { + commitContainer = function(finishedWork: Fiber) { + // Noop + }; + } else { + const {replaceContainer} = config.persistence; + commitContainer = function(finishedWork: Fiber) { + switch (finishedWork.tag) { + case ClassComponent: { + return; + } + case HostComponent: { + 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.', + ); + } + } + }; + } return { commitResetTextContent(finishedWork: Fiber) {}, commitPlacement(finishedWork: Fiber) {}, @@ -281,7 +318,9 @@ module.exports = function( commitNestedUnmounts(current); detachFiber(current); }, - commitWork(current: Fiber | null, finishedWork: Fiber) {}, + commitWork(current: Fiber | null, finishedWork: Fiber) { + commitContainer(finishedWork); + }, commitLifeCycles, commitAttachRef, commitDetachRef, From fee0aa1237295bf68b7ccbeae0b0c5226a123618 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 15:06:17 -0700 Subject: [PATCH 07/16] Unify cloneOrRecycle Originally I tried to make the recyclable instance nullable but Flow didn't like that and it's kind of sketchy since the instance type might not be nullable. However, the real difference which one we call is depending on whether they are equal. We can just offload that to the renderer. Most of them won't need to know about this at all since they'll always clone or just create new. The ones that do know now have to be careful to compare them so they don't reuse an existing instance but that's probably fine to simplify the implementation and API. --- .../native-cs/ReactNativeCSFiberEntry.js | 16 +----- .../shared/fiber/ReactFiberCompleteWork.js | 50 ++++++------------- .../shared/fiber/ReactFiberReconciler.js | 12 +---- 3 files changed, 16 insertions(+), 62 deletions(-) diff --git a/src/renderers/native-cs/ReactNativeCSFiberEntry.js b/src/renderers/native-cs/ReactNativeCSFiberEntry.js index 1b61bfa7889..4fe83bd8577 100644 --- a/src/renderers/native-cs/ReactNativeCSFiberEntry.js +++ b/src/renderers/native-cs/ReactNativeCSFiberEntry.js @@ -160,26 +160,12 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ newProps: Props, internalInstanceHandle: Object, keepChildren: boolean, - ): Instance { - return 0; - }, - cloneInstanceOrRecycle( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, recyclableInstance: null | Instance, ): Instance { return 0; }, - cloneContainer(container: Container): Container { - return 0; - }, - cloneContainerOrRecycle( + cloneContainer( container: Container, recyclableContainer: Container, ): Container { diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index a5d1c658de8..520e4c291f7 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -219,9 +219,7 @@ module.exports = function( // Persistent host tree mode const { cloneInstance, - cloneInstanceOrRecycle, cloneContainer, - cloneContainerOrRecycle, appendInititalChildToContainer, finalizeContainerChildren, } = persistence; @@ -269,16 +267,10 @@ module.exports = function( // Note that this might release a previous clone. portalOrRoot.pendingContainerInfo = currentContainer; } else { - let newContainer; - if (currentContainer === recyclableContainer) { - // We can't recycle the current, we'll need to clone. - newContainer = cloneContainer(currentContainer); - } else { - newContainer = cloneContainerOrRecycle( - currentContainer, - recyclableContainer, - ); - } + let newContainer = cloneContainer( + currentContainer, + recyclableContainer, + ); if (finalizeContainerChildren(newContainer)) { markUpdate(workInProgress); } @@ -308,30 +300,16 @@ module.exports = function( workInProgress.stateNode = currentInstance; } else { let recyclableInstance = workInProgress.stateNode; - let newInstance; - if (currentInstance === recyclableInstance) { - // We can't recycle the current, we'll need to clone. - newInstance = cloneInstance( - currentInstance, - updatePayload, - type, - oldProps, - newProps, - workInProgress, - childrenUnchanged, - ); - } else { - newInstance = cloneInstanceOrRecycle( - currentInstance, - updatePayload, - type, - oldProps, - newProps, - workInProgress, - childrenUnchanged, - recyclableInstance, - ); - } + let newInstance = cloneInstance( + currentInstance, + updatePayload, + type, + oldProps, + newProps, + workInProgress, + childrenUnchanged, + recyclableInstance, + ); if ( finalizeInitialChildren( newInstance, diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index a4cb0502c88..2fcff4aaa7c 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -141,20 +141,10 @@ type PersistentUpdatesHostConfig = { newProps: P, internalInstanceHandle: OpaqueHandle, keepChildren: boolean, - ): I, - cloneInstanceOrRecycle( - instance: I, - updatePayload: null | PL, - type: T, - oldProps: P, - newProps: P, - internalInstanceHandle: OpaqueHandle, - keepChildren: boolean, recyclableInstance: I, ): I, - cloneContainer(container: C): C, - cloneContainerOrRecycle(container: C, recyclableContainer: C): C, + cloneContainer(container: C, recyclableContainer: C): C, appendInititalChildToContainer(container: C, child: I | TI): void, finalizeContainerChildren(container: C): void, From d87ec824873e4b365b5bf0661d899e263436df13 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 15:30:57 -0700 Subject: [PATCH 08/16] Add persistent noop renderer for testing --- src/renderers/noop/ReactNoopEntry.js | 64 +++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 89bfc33c2d7..5b828362166 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,7 +176,10 @@ var NoopRenderer = ReactFiberReconciler({ now(): number { return elapsedTimeInMs; }, +}; +var NoopRenderer = ReactFiberReconciler({ + ...SharedHostConfig, mutation: { commitMount(instance: Instance, type: string, newProps: Props): void { // Noop @@ -211,8 +214,52 @@ var NoopRenderer = ReactFiberReconciler({ }, }); +var PersistentNoopRenderer = ReactFiberReconciler({ + ...SharedHostConfig, + persistence: { + 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: appendChild, + + finalizeContainerChildren(container: Container): void {}, + + replaceContainer(oldContainer: Container, newContainer: Container): void { + rootContainers.set(oldContainer.rootID, newContainer); + }, + }, +}); + var rootContainers = new Map(); var roots = new Map(); +var persistentRoots = new Map(); var DEFAULT_ROOT_ID = ''; let yieldedValues = null; @@ -275,6 +322,21 @@ var ReactNoop = { NoopRenderer.updateContainer(element, root, null, callback); }, + renderToPersistentRootWithID( + element: React$Element, + rootID: string, + callback: ?Function, + ) { + 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 root = roots.get(rootID); if (root) { From a6d4bdeca1956a157976ba4d0d0b143b3b156a51 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 17:20:59 -0700 Subject: [PATCH 09/16] Update build size --- scripts/rollup/results.json | 96 ++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 48 deletions(-) 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 From da330dc697399a702a0ee19a95c28b25f1a7b768 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 22:03:43 -0700 Subject: [PATCH 10/16] Add basic persistent tree test --- .../fiber/__tests__/ReactPersistent-test.js | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/renderers/shared/fiber/__tests__/ReactPersistent-test.js 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..1ccab9571f0 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -0,0 +1,67 @@ +/** + * 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('ReactPersistent', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + + 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())]); + }); +}); From 87ec057898c4b1119659b0d985296a5d6f1cc81f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 22:51:15 -0700 Subject: [PATCH 11/16] Test bail out This adds a test for bailouts. This revealed a subtle bug. We don't set the return pointer when stepping into newly created fibers because there can only be one. However, since I'm reusing this mechanism for persistent updates, I'll need to set the return pointer because a bailed out tree won't have the right return pointer. --- src/renderers/noop/ReactNoopEntry.js | 7 +++- .../shared/fiber/ReactFiberCompleteWork.js | 4 ++ .../fiber/__tests__/ReactPersistent-test.js | 37 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 5b828362166..cba0c28e0ae 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -247,7 +247,12 @@ var PersistentNoopRenderer = ReactFiberReconciler({ return {rootID: container.rootID, children: []}; }, - appendInititalChildToContainer: appendChild, + appendInititalChildToContainer( + parentInstance: Container, + child: Instance | TextInstance, + ) { + parentInstance.children.push(child); + }, finalizeContainerChildren(container: Container): void {}, diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 520e4c291f7..05e6437b162 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -163,6 +163,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; } @@ -175,6 +176,7 @@ module.exports = function( } node = node.return; } + node.sibling.return = node.return; node = node.sibling; } } @@ -241,6 +243,7 @@ module.exports = function( // the portal directly. } else if (node.child !== null) { node = node.child; + node.child.return = node; continue; } if (node === workInProgress) { @@ -252,6 +255,7 @@ module.exports = function( } node = node.return; } + node.sibling.return = node.return; node = node.sibling; } }; diff --git a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js index 1ccab9571f0..01316a97919 100644 --- a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -64,4 +64,41 @@ describe('ReactPersistent', () => { 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]); + }); }); From ed0cba42e0fac4b249abea8cf505f48182f18e25 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 17 Oct 2017 23:35:49 -0700 Subject: [PATCH 12/16] Test persistent text nodes Found another bug. --- .../shared/fiber/ReactFiberCommitWork.js | 3 +++ .../fiber/__tests__/ReactPersistent-test.js | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 4afbc2ee425..72fa5cbf2e6 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -288,6 +288,9 @@ module.exports = function( case HostComponent: { return; } + case HostText: { + return; + } case HostRoot: case HostPortal: { const portalOrRoot: {containerInfo: C, pendingContainerInfo: C} = diff --git a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js index 01316a97919..a8b5f8a5c73 100644 --- a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -101,4 +101,27 @@ describe('ReactPersistent', () => { // 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())]); + }); }); From 08015d6899932467dc2e21ec4b7a51c3340db4ff Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 18 Oct 2017 00:51:13 -0700 Subject: [PATCH 13/16] Add persistent portal test This creates a bit of an unfortunate feature testing in the unmount branch. That's because we want to trigger nested host deletions in portals in the mutation mode. --- .../shared/fiber/ReactFiberCommitWork.js | 14 +++++- .../shared/fiber/ReactFiberCompleteWork.js | 2 +- .../fiber/__tests__/ReactPersistent-test.js | 46 +++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 72fa5cbf2e6..8bafd5d9d5f 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -162,6 +162,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 +226,9 @@ module.exports = function( // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. - unmountHostComponents(current); + if (config.mutation) { + unmountHostComponents(current); + } return; } } @@ -239,7 +245,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 + (!config.mutation || node.tag !== HostPortal) + ) { node.child.return = node; node = node.child; continue; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 05e6437b162..a5029be9945 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -242,8 +242,8 @@ module.exports = function( // down its children. Instead, we'll get insertions from each child in // the portal directly. } else if (node.child !== null) { - node = node.child; node.child.return = node; + node = node.child; continue; } if (node === workInProgress) { diff --git a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js index a8b5f8a5c73..e7b6c6bc0b9 100644 --- a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -11,12 +11,14 @@ var React; var ReactNoop; +let ReactPortal; describe('ReactPersistent', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); + ReactPortal = require('ReactPortal'); }); const DEFAULT_ROOT_ID = 'persistent-test'; @@ -124,4 +126,48 @@ describe('ReactPersistent', () => { expect(originalChildren).toEqual([div('Hello', span())]); }); + + it('supports portals', () => { + function Parent(props) { + return
{props.children}
; + } + + 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()]); + + 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('Hello ', 'World')]); + + expect(originalChildren).toEqual([div()]); + expect(originalPortalChildren).toEqual([div()]); + }); }); From ac265ec744eeed25b92989668ae6ef60fd50e3b2 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 18 Oct 2017 01:53:34 -0700 Subject: [PATCH 14/16] Don't consider container when determining portal identity Basically, we can't use the container to determine if we should keep identity and update an existing portal instead of recreate it. Because for persistent containers, there is no permanent identity. This makes it kind of strange to even use portals in this mode. It's probably more ideal to have another concept that has permanent identity rather than trying to swap out containers. --- src/renderers/shared/fiber/ReactChildFiber.js | 6 +++-- .../fiber/__tests__/ReactPersistent-test.js | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) 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/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js index e7b6c6bc0b9..e910e7364e6 100644 --- a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -132,8 +132,21 @@ describe('ReactPersistent', () => { return
{props.children}
; } + function BailoutSpan() { + return ; + } + + class BailoutTest extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return ; + } + } + function Child(props) { - return
{props.children}
; + return
{props.children}
; } const portalID = 'persistent-portal-test'; const portalContainer = {rootID: portalID, children: []}; @@ -149,7 +162,7 @@ describe('ReactPersistent', () => { var originalChildren = getChildren(); expect(originalChildren).toEqual([div()]); var originalPortalChildren = ReactNoop.getChildren(portalID); - expect(originalPortalChildren).toEqual([div()]); + expect(originalPortalChildren).toEqual([div(span())]); render( @@ -165,9 +178,14 @@ describe('ReactPersistent', () => { var newChildren = getChildren(); expect(newChildren).toEqual([div()]); var newPortalChildren = ReactNoop.getChildren(portalID); - expect(newPortalChildren).toEqual([div('Hello ', 'World')]); + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]); expect(originalChildren).toEqual([div()]); - expect(originalPortalChildren).toEqual([div()]); + expect(originalPortalChildren).toEqual([div(span())]); + + // Reused portal children should have reference equality + expect(newPortalChildren[0].children[0]).toBe( + originalPortalChildren[0].children[0], + ); }); }); From a6ba5fcbacd99eab094660ae85124fc130b04e50 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 18 Oct 2017 02:03:29 -0700 Subject: [PATCH 15/16] Clear portals when the portal is deleted When a portal gets deleted we need to create a new empty container and replace the current one with the empty one. --- src/renderers/shared/fiber/ReactFiberCommitWork.js | 14 +++++++++++++- .../shared/fiber/__tests__/ReactPersistent-test.js | 10 ++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 8bafd5d9d5f..67036109246 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -228,6 +228,8 @@ module.exports = function( // the portal will get pushed immediately. if (config.mutation) { unmountHostComponents(current); + } else if (config.persistence) { + emptyPortalContainer(current); } return; } @@ -289,7 +291,17 @@ module.exports = function( // Noop }; } else { - const {replaceContainer} = config.persistence; + const {replaceContainer, cloneContainer} = config.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: { diff --git a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js index e910e7364e6..ba11d649a2c 100644 --- a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -187,5 +187,15 @@ describe('ReactPersistent', () => { 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')]); }); }); From 419d0014a6b389a4f9a3c6c1f7a2e944d30d4b3a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 18 Oct 2017 21:45:21 +0100 Subject: [PATCH 16/16] Add renderer mode flags for dead code elimination --- scripts/rollup/bundles.js | 1 + .../native-cs/ReactNativeCSFeatureFlags.js | 24 ++ .../native-cs/__tests__/ReactNativeCS-test.js | 2 + src/renderers/noop/ReactNoopEntry.js | 196 +++++---- .../shared/fiber/ReactFiberCommitWork.js | 90 +++-- .../shared/fiber/ReactFiberCompleteWork.js | 377 +++++++++--------- .../fiber/__tests__/ReactPersistent-test.js | 6 + .../shared/utils/ReactFeatureFlags.js | 9 + 8 files changed, 404 insertions(+), 301 deletions(-) create mode 100644 src/renderers/native-cs/ReactNativeCSFeatureFlags.js 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/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/__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 cba0c28e0ae..7e67fcccb25 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -178,89 +178,105 @@ var SharedHostConfig = { }, }; -var NoopRenderer = ReactFiberReconciler({ - ...SharedHostConfig, - 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 {}, - }, -}); - -var PersistentNoopRenderer = ReactFiberReconciler({ - ...SharedHostConfig, - persistence: { - 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; - }, +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, + + 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: []}; - }, + cloneContainer( + container: Container, + recyclableContainer: Container, + ): Container { + return {rootID: container.rootID, children: []}; + }, - appendInititalChildToContainer( - parentInstance: Container, - child: Instance | TextInstance, - ) { - parentInstance.children.push(child); - }, + appendInititalChildToContainer( + parentInstance: Container, + child: Instance | TextInstance, + ) { + parentInstance.children.push(child); + }, - finalizeContainerChildren(container: Container): void {}, + finalizeContainerChildren(container: Container): void {}, - replaceContainer(oldContainer: Container, newContainer: Container): void { - rootContainers.set(oldContainer.rootID, newContainer); - }, + 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(); @@ -317,6 +333,7 @@ var ReactNoop = { rootID: string, callback: ?Function, ) { + const NoopRenderer = getRenderer(); let root = roots.get(rootID); if (!root) { const container = {rootID: rootID, children: []}; @@ -332,6 +349,7 @@ var ReactNoop = { rootID: string, callback: ?Function, ) { + const PersistentNoopRenderer = getPersistentRenderer(); let root = persistentRoots.get(rootID); if (!root) { const container = {rootID: rootID, children: []}; @@ -343,6 +361,7 @@ var ReactNoop = { }, unmountRootWithID(rootID: string) { + const NoopRenderer = getRenderer(); const root = roots.get(rootID); if (root) { NoopRenderer.updateContainer(null, root, null, () => { @@ -355,6 +374,7 @@ var ReactNoop = { findInstance( componentOrElement: Element | ?React$Component, ): null | Instance | TextInstance { + const NoopRenderer = getRenderer(); if (componentOrElement == null) { return null; } @@ -431,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/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 67036109246..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) { @@ -226,9 +227,12 @@ module.exports = function( // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. - if (config.mutation) { + if (ReactFeatureFlags.enableMutatingReconciler && mutation) { unmountHostComponents(current); - } else if (config.persistence) { + } else if ( + ReactFeatureFlags.enablePersistentReconciler && + persistence + ) { emptyPortalContainer(current); } return; @@ -250,7 +254,7 @@ module.exports = function( if ( node.child !== null && // Drill down into portals only if we use mutation since that branch is recursive - (!config.mutation || node.tag !== HostPortal) + (!mutation || node.tag !== HostPortal) ) { node.child.return = node; node = node.child; @@ -284,14 +288,10 @@ module.exports = function( } } - if (!config.mutation) { + if (!mutation) { let commitContainer; - if (!config.persistence) { - commitContainer = function(finishedWork: Fiber) { - // Noop - }; - } else { - const {replaceContainer, cloneContainer} = config.persistence; + if (persistence) { + const {replaceContainer, cloneContainer} = persistence; var emptyPortalContainer = function(current: Fiber) { const portal: {containerInfo: C, pendingContainerInfo: C} = current.stateNode; @@ -334,24 +334,36 @@ module.exports = function( } } }; + } 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.'); } - 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, - }; } - const { commitMount, commitUpdate, @@ -363,7 +375,7 @@ module.exports = function( insertInContainerBefore, removeChild, removeChildFromContainer, - } = config.mutation; + } = mutation; function getHostParentFiber(fiber: Fiber): Fiber { let parent = fiber.return; @@ -663,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 a5029be9945..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'); @@ -185,202 +186,214 @@ module.exports = function( let updateHostComponent; let updateHostText; if (mutation) { - // 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 if (persistence) { - // 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 (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); } - if (node === workInProgress) { - return; + }; + 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); } - while (node.sibling === null) { - if (node.return === null || node.return === 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; } - node = node.return; + while (node.sibling === null) { + if (node.return === null || node.return === workInProgress) { + return; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; } - 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; + }; + 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)) { + 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); } - 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, + }; + 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, - rootContainerInstance, - ) - ) { - markUpdate(workInProgress); + 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); + } } - 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. + }; + 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 { - // 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 { - // 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 - }; + 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( diff --git a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js index ba11d649a2c..bd2043c7ee3 100644 --- a/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactPersistent-test.js @@ -16,6 +16,12 @@ 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'); 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__) {