mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Core: Add "state reconciliation" to commit phase, pre-layout
Summary: This implements proposal #2 in our State architecture doc: https://fb.quip.com/bm2EAVwL7jQ5 Problem description: see the text in the comment of TreeStateReconciliation.h Solution: see also comments in TreeStateReconciliation.h. Changelog: [internal] Reviewed By: mdvacca Differential Revision: D19617329 fbshipit-source-id: 845fb5fe27f2591be433b6d77799707b3516fb1a
This commit is contained in:
committed by
Facebook Github Bot
parent
1f88b0db2e
commit
27981ad991
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "ShadowNode.h"
|
||||
#include "ShadowNodeFragment.h"
|
||||
|
||||
#include <better/small_vector.h>
|
||||
|
||||
@@ -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<std::string>(revision_) +
|
||||
return "r" + folly::to<std::string>(revision_) + "/sr" +
|
||||
folly::to<std::string>(stateRevision_) + "/s" +
|
||||
folly::to<std::string>(state_ ? state_->getRevision() : 0) +
|
||||
(getSealed() ? "/sealed" : "");
|
||||
}
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <react/mounting/ShadowViewMutation.h>
|
||||
|
||||
#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<RootShadowNode>(*reconciledNode, ShadowNodeFragment{});
|
||||
}
|
||||
|
||||
// Layout nodes
|
||||
std::vector<LayoutableShadowNode const *> 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{};
|
||||
|
||||
@@ -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<std::pair<SharedShadowNode, UnsharedShadowNode>>;
|
||||
|
||||
/**
|
||||
* 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<ShadowNode::ListOfShared>();
|
||||
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
|
||||
@@ -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 <react/core/ShadowNode.h>
|
||||
#include <react/core/ShadowNodeFragment.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user