diff --git a/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 8224995a164..a0b5b2dc3c0 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -17,6 +17,7 @@ #include #include "ShadowTreeDelegate.h" +#include "TreeStateReconciliation.h" namespace facebook { namespace react { @@ -302,11 +303,22 @@ bool ShadowTree::tryCommit( } if (enableStateReconciliation) { - auto updatedNewRootShadowNode = - progressState(*newRootShadowNode, *oldRootShadowNode); - if (updatedNewRootShadowNode) { - newRootShadowNode = - std::static_pointer_cast(updatedNewRootShadowNode); + if (enableNewStateReconciliation_) { + auto updatedNewRootShadowNode = + progressState(*newRootShadowNode, *oldRootShadowNode); + if (updatedNewRootShadowNode) { + newRootShadowNode = + std::static_pointer_cast(updatedNewRootShadowNode); + } + } else { + // Compare state revisions of old and new root + // Children of the root node may be mutated in-place + UnsharedShadowNode reconciledNode = + reconcileStateWithTree(newRootShadowNode.get(), oldRootShadowNode); + if (reconciledNode != nullptr) { + newRootShadowNode = std::make_shared( + *reconciledNode, ShadowNodeFragment{}); + } } } diff --git a/ReactCommon/react/renderer/mounting/ShadowTree.h b/ReactCommon/react/renderer/mounting/ShadowTree.h index 06e4e6c3e43..70155de275f 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTree.h +++ b/ReactCommon/react/renderer/mounting/ShadowTree.h @@ -81,6 +81,13 @@ class ShadowTree final { MountingCoordinator::Shared getMountingCoordinator() const; + /* + * Temporary. + * Do not use. + */ + void setEnableNewStateReconciliation(bool value) { + enableNewStateReconciliation_ = value; + } void setEnableReparentingDetection(bool value) { enableReparentingDetection_ = value; } diff --git a/ReactCommon/react/renderer/mounting/TreeStateReconciliation.cpp b/ReactCommon/react/renderer/mounting/TreeStateReconciliation.cpp new file mode 100644 index 00000000000..58017f16a3c --- /dev/null +++ b/ReactCommon/react/renderer/mounting/TreeStateReconciliation.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TreeStateReconciliation.h" + +namespace facebook { +namespace react { + +using ChangedShadowNodePairs = + std::vector>; + +/** + * Clones any children in the subtree that need to be cloned, and adds those to + * the `changedPairs` vector argument. + */ +static ChangedShadowNodePairs reconcileStateWithChildren( + SharedShadowNodeList const &newChildren, + SharedShadowNodeList const &oldChildren) { + ChangedShadowNodePairs changedPairs; + // Find children that are the same family in both trees. + // We only want to find nodes that existing in the new tree - if they + // don't exist in the new tree, they're being deleted; if they don't exist + // in the old tree, they're new. We don't need to deal with either of those + // cases here. + // Currently we use a naive double loop - this could be improved, but we need + // to be able to handle cases where nodes are entirely reordered, for + // instance. + for (auto const &child : newChildren) { + auto const oldChild = std::find_if( + oldChildren.begin(), oldChildren.end(), [&](const auto &el) { + return ShadowNode::sameFamily(*child, *el); + }); + + if (oldChild != oldChildren.end()) { + UnsharedShadowNode newChild = + reconcileStateWithTree(child.get(), *oldChild); + if (newChild != nullptr) { + changedPairs.push_back(std::make_pair(child, newChild)); + } + } + }; + + return changedPairs; +} + +UnsharedShadowNode reconcileStateWithTree( + ShadowNode const *newNode, + SharedShadowNode committedNode) { + // If the revisions on the node are the same, we can finish here. + // Subtrees are guaranteed to be identical at this point, too. + if (committedNode->getStateRevision() <= newNode->getStateRevision()) { + return nullptr; + } + + // If we got this fair, we're guaranteed that the state of 1) this node, + // and/or 2) some descendant node is out-of-date and must be reconciled. + // This requires traversing all children, and we must at *least* clone + // this node, whether or not we clone and update any children. + auto const &newChildren = newNode->getChildren(); + auto const &oldChildren = committedNode->getChildren(); + auto const changedPairs = + reconcileStateWithChildren(newChildren, oldChildren); + + ShadowNode::SharedListOfShared clonedChildren = + ShadowNodeFragment::childrenPlaceholder(); + + // If any children were cloned, we need to recreate the child list. + // This won't cause any children to be cloned that weren't already cloned - + // it just collects all children, cloned or uncloned, into a new list. + if (!changedPairs.empty()) { + ShadowNode::UnsharedListOfShared newList = + std::make_shared(); + for (std::size_t i = 0, j = 0; i < newChildren.size(); ++i) { + if (j < changedPairs.size() && changedPairs[j].first == newChildren[i]) { + newList->push_back(changedPairs[j].second); + ++j; + } else { + newList->push_back(newChildren[i]); + } + } + clonedChildren = newList; + } + + return newNode->clone({/* .props = */ ShadowNodeFragment::propsPlaceholder(), + /* .children = */ clonedChildren, + /* .state = */ newNode->getMostRecentState()}); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/mounting/TreeStateReconciliation.h b/ReactCommon/react/renderer/mounting/TreeStateReconciliation.h new file mode 100644 index 00000000000..ff95b7e763d --- /dev/null +++ b/ReactCommon/react/renderer/mounting/TreeStateReconciliation.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +namespace facebook { +namespace react { + +/** + * Problem Description: because of C++ State, the React Native C++ ShadowTree + * can diverge from the ReactJS ShadowTree; ReactJS communicates all tree + * changes to C++, but C++ state commits are not propagated to ReactJS (ReactJS + * may or may not clone nodes with state changes, but it has no way of knowing + * if it /should/ clone those nodes; so those clones may never happen). This + * causes a number of problems. This function resolves the problem by taking a + * candidate tree being committed, and sees if any State changes need to be + * applied to it. If any changes need to be made, a new ShadowNode is returned; + * otherwise, nullptr is returned if the node is already consistent with the + * latest tree, including all state changes. + * + * This should be called during the commit phase, pre-layout and pre-diff. + */ +UnsharedShadowNode reconcileStateWithTree( + ShadowNode const *newNode, + SharedShadowNode committedNode); + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 39d733264a8..58128870bd0 100644 --- a/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -108,11 +108,15 @@ Scheduler::Scheduler( #ifdef ANDROID enableReparentingDetection_ = reactNativeConfig_->getBool( "react_fabric:enable_reparenting_detection_android"); + enableNewStateReconciliation_ = reactNativeConfig_->getBool( + "react_fabric:enable_new_state_reconciliation_android"); removeOutstandingSurfacesOnDestruction_ = reactNativeConfig_->getBool( "react_fabric:remove_outstanding_surfaces_on_destruction_android"); #else enableReparentingDetection_ = reactNativeConfig_->getBool( "react_fabric:enable_reparenting_detection_ios"); + enableNewStateReconciliation_ = reactNativeConfig_->getBool( + "react_fabric:enable_new_state_reconciliation_ios"); removeOutstandingSurfacesOnDestruction_ = reactNativeConfig_->getBool( "react_fabric:remove_outstanding_surfaces_on_destruction_ios"); #endif @@ -188,6 +192,8 @@ void Scheduler::startSurface( mountingOverrideDelegate, enableReparentingDetection_); + shadowTree->setEnableNewStateReconciliation(enableNewStateReconciliation_); + auto uiManager = uiManager_; uiManager->getShadowTreeRegistry().add(std::move(shadowTree)); diff --git a/ReactCommon/react/renderer/scheduler/Scheduler.h b/ReactCommon/react/renderer/scheduler/Scheduler.h index 3b5693cb724..c025b39a3b9 100644 --- a/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -138,6 +138,7 @@ class Scheduler final : public UIManagerDelegate { * Temporary flags. */ bool enableReparentingDetection_{false}; + bool enableNewStateReconciliation_{false}; bool removeOutstandingSurfacesOnDestruction_{false}; };