diff --git a/ReactCommon/fabric/core/shadownode/ShadowNode.cpp b/ReactCommon/fabric/core/shadownode/ShadowNode.cpp index 581c8de8596df7..fb1a960c3dd650 100644 --- a/ReactCommon/fabric/core/shadownode/ShadowNode.cpp +++ b/ReactCommon/fabric/core/shadownode/ShadowNode.cpp @@ -6,6 +6,7 @@ */ #include "ShadowNode.h" +#include "ShadowNodeFragment.h" #include @@ -29,6 +30,21 @@ bool ShadowNode::sameFamily(const ShadowNode &first, const ShadowNode &second) { #pragma mark - Constructors +static int computeStateRevision( + const State::Shared &state, + const SharedShadowNodeSharedList &children) { + int fragmentStateRevision = state ? state->getRevision() : 0; + int childrenSum = 0; + + if (children) { + for (const auto &child : *children) { + childrenSum += child->getStateRevision(); + } + } + + return fragmentStateRevision + childrenSum; +} + ShadowNode::ShadowNode( ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family, @@ -42,6 +58,7 @@ ShadowNode::ShadowNode( fragment.children ? fragment.children : emptySharedShadowNodeSharedList()), state_(fragment.state), + stateRevision_(computeStateRevision(state_, children_)), family_(family), traits_(traits) { assert(props_); @@ -67,6 +84,7 @@ ShadowNode::ShadowNode( state_( fragment.state ? fragment.state : sourceShadowNode.getMostRecentState()), + stateRevision_(computeStateRevision(state_, children_)), family_(sourceShadowNode.family_), traits_(sourceShadowNode.traits_) { @@ -217,6 +235,10 @@ ShadowNodeFamily const &ShadowNode::getFamily() const { return *family_; } +int ShadowNode::getStateRevision() const { + return stateRevision_; +} + #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE @@ -225,7 +247,9 @@ std::string ShadowNode::getDebugName() const { } std::string ShadowNode::getDebugValue() const { - return "r" + folly::to(revision_) + + return "r" + folly::to(revision_) + "/sr" + + folly::to(stateRevision_) + "/s" + + folly::to(state_ ? state_->getRevision() : 0) + (getSealed() ? "/sealed" : ""); } diff --git a/ReactCommon/fabric/core/shadownode/ShadowNode.h b/ReactCommon/fabric/core/shadownode/ShadowNode.h index adf8f1ad02c795..08de4454529854 100644 --- a/ReactCommon/fabric/core/shadownode/ShadowNode.h +++ b/ReactCommon/fabric/core/shadownode/ShadowNode.h @@ -144,6 +144,8 @@ class ShadowNode : public virtual Sealable, */ void setMounted(bool mounted) const; + int getStateRevision() const; + #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE @@ -167,6 +169,15 @@ class ShadowNode : public virtual Sealable, private: friend ShadowNodeFamily; + + /** + * This number is deterministically, statelessly recomputable (it's dependent + * only on the immutable properties stored in this class). It tells us the + * version of the state of the entire subtree, including this component and + * all descendants. + */ + int const stateRevision_; + /* * Clones the list of children (and creates a new `shared_ptr` to it) if * `childrenAreShared_` flag is `true`. diff --git a/ReactCommon/fabric/mounting/ShadowTree.cpp b/ReactCommon/fabric/mounting/ShadowTree.cpp index 0d55bbfbae8fc9..59d414943e98f7 100644 --- a/ReactCommon/fabric/mounting/ShadowTree.cpp +++ b/ReactCommon/fabric/mounting/ShadowTree.cpp @@ -17,6 +17,7 @@ #include #include "ShadowTreeDelegate.h" +#include "TreeStateReconciliation.h" namespace facebook { namespace react { @@ -165,6 +166,16 @@ bool ShadowTree::tryCommit(ShadowTreeCommitTransaction transaction) const { return false; } + // 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{}); + } + + // Layout nodes std::vector affectedLayoutableNodes{}; affectedLayoutableNodes.reserve(1024); @@ -172,6 +183,7 @@ bool ShadowTree::tryCommit(ShadowTreeCommitTransaction transaction) const { newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes); telemetry.didLayout(); + // Seal the shadow node so it can no longer be mutated newRootShadowNode->sealRecursive(); auto revisionNumber = ShadowTreeRevision::Number{}; diff --git a/ReactCommon/fabric/mounting/TreeStateReconciliation.cpp b/ReactCommon/fabric/mounting/TreeStateReconciliation.cpp new file mode 100644 index 00000000000000..53afb02fda6016 --- /dev/null +++ b/ReactCommon/fabric/mounting/TreeStateReconciliation.cpp @@ -0,0 +1,97 @@ +/* + * 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. + * + * @param parent + * @param newChildren + * @param oldChildren + * @param changedPairs + */ +static void reconcileStateWithChildren( + ShadowNode const *parent, + const SharedShadowNodeList &newChildren, + const SharedShadowNodeList &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 (int i = 0; i < newChildren.size(); i++) { + bool found = false; + for (int j = 0; j < oldChildren.size() && !found; j++) { + if (ShadowNode::sameFamily(*newChildren[i], *oldChildren[j])) { + UnsharedShadowNode newChild = + reconcileStateWithTree(newChildren[i].get(), oldChildren[j]); + if (newChild != nullptr) { + changedPairs.push_back(std::make_pair(newChildren[i], newChild)); + } + found = true; + } + } + } +} + +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. + const auto &newChildren = newNode->getChildren(); + const auto &oldChildren = committedNode->getChildren(); + ChangedShadowNodePairs changedPairs; + reconcileStateWithChildren(newNode, newChildren, oldChildren, changedPairs); + + 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.size() > 0) { + ShadowNode::UnsharedListOfShared newList = + std::make_shared(); + for (int 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/fabric/mounting/TreeStateReconciliation.h b/ReactCommon/fabric/mounting/TreeStateReconciliation.h new file mode 100644 index 00000000000000..6775df1906d6a4 --- /dev/null +++ b/ReactCommon/fabric/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