mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
09b9422516
Summary: Unfortunately, parsing some props requires stateful context - namely, PlatformColor on Android. We explored several different options but they all seemed inferior to the approach of using ContextContainer, and most would require using global state. By introducing this change everywhere as early as possible, we can avoid later pain. It is likely that some prop, on some platform, will require this mechanism. We'll be ready for it! Because we can pass a constref of the ContextContainer through to all props and because the context and context data is never retained by prop parsers, perf and memory hit should be ~0. This diff contains core changes only. Leaf changes to all props structs and conversions files will be in next diff(s). Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D29838789 fbshipit-source-id: f5090e7f02eb6e8fbe0ef4dd201e7d12104a3e3c
463 lines
14 KiB
C++
463 lines
14 KiB
C++
/*
|
|
* 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 "ShadowTree.h"
|
|
|
|
#include <react/debug/react_native_assert.h>
|
|
#include <react/renderer/components/root/RootComponentDescriptor.h>
|
|
#include <react/renderer/components/view/ViewShadowNode.h>
|
|
#include <react/renderer/core/LayoutContext.h>
|
|
#include <react/renderer/core/LayoutPrimitives.h>
|
|
#include <react/renderer/debug/SystraceSection.h>
|
|
#include <react/renderer/mounting/ShadowTreeRevision.h>
|
|
#include <react/renderer/mounting/ShadowViewMutation.h>
|
|
#include <react/renderer/telemetry/TransactionTelemetry.h>
|
|
|
|
#include "ShadowTreeDelegate.h"
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
using CommitStatus = ShadowTree::CommitStatus;
|
|
using CommitMode = ShadowTree::CommitMode;
|
|
|
|
/*
|
|
* Generates (possibly) a new tree where all nodes with non-obsolete `State`
|
|
* objects. If all `State` objects in the tree are not obsolete for the moment
|
|
* of calling, the function returns `nullptr` (as an indication that no
|
|
* additional work is required).
|
|
*/
|
|
static ShadowNode::Unshared progressState(ShadowNode const &shadowNode) {
|
|
auto isStateChanged = false;
|
|
auto areChildrenChanged = false;
|
|
|
|
auto newState = shadowNode.getState();
|
|
if (newState) {
|
|
newState = newState->getMostRecentStateIfObsolete();
|
|
if (newState) {
|
|
isStateChanged = true;
|
|
}
|
|
}
|
|
|
|
auto newChildren = ShadowNode::ListOfShared{};
|
|
if (!shadowNode.getChildren().empty()) {
|
|
auto index = size_t{0};
|
|
for (auto const &childNode : shadowNode.getChildren()) {
|
|
auto newChildNode = progressState(*childNode);
|
|
if (newChildNode) {
|
|
if (!areChildrenChanged) {
|
|
// Making a copy before the first mutation.
|
|
newChildren = shadowNode.getChildren();
|
|
}
|
|
newChildren[index] = newChildNode;
|
|
areChildrenChanged = true;
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
|
|
if (!areChildrenChanged && !isStateChanged) {
|
|
return nullptr;
|
|
}
|
|
|
|
return shadowNode.clone({
|
|
ShadowNodeFragment::propsPlaceholder(),
|
|
areChildrenChanged ? std::make_shared<ShadowNode::ListOfShared const>(
|
|
std::move(newChildren))
|
|
: ShadowNodeFragment::childrenPlaceholder(),
|
|
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
|
|
});
|
|
}
|
|
|
|
/*
|
|
* An optimized version of the previous function (and relies on it).
|
|
* The function uses a given base tree to exclude unchanged (equal) parts
|
|
* of the three from the traversing.
|
|
*/
|
|
static ShadowNode::Unshared progressState(
|
|
ShadowNode const &shadowNode,
|
|
ShadowNode const &baseShadowNode) {
|
|
// The intuition behind the complexity:
|
|
// - A very few nodes have associated state, therefore it's mostly reading and
|
|
// it only writes when state objects were found obsolete;
|
|
// - Most before-after trees are aligned, therefore most tree branches will be
|
|
// skipped;
|
|
// - If trees are significantly different, any other algorithm will have
|
|
// close to linear complexity.
|
|
|
|
auto isStateChanged = false;
|
|
auto areChildrenChanged = false;
|
|
|
|
auto newState = shadowNode.getState();
|
|
if (newState) {
|
|
newState = newState->getMostRecentStateIfObsolete();
|
|
if (newState) {
|
|
isStateChanged = true;
|
|
}
|
|
}
|
|
|
|
auto &children = shadowNode.getChildren();
|
|
auto &baseChildren = baseShadowNode.getChildren();
|
|
auto newChildren = ShadowNode::ListOfShared{};
|
|
|
|
auto childrenSize = children.size();
|
|
auto baseChildrenSize = baseChildren.size();
|
|
auto index = size_t{0};
|
|
|
|
// Stage 1: Aligned part.
|
|
for (index = 0; index < childrenSize && index < baseChildrenSize; index++) {
|
|
const auto &childNode = *children.at(index);
|
|
const auto &baseChildNode = *baseChildren.at(index);
|
|
|
|
if (&childNode == &baseChildNode) {
|
|
// Nodes are identical, skipping.
|
|
continue;
|
|
}
|
|
|
|
if (!ShadowNode::sameFamily(childNode, baseChildNode)) {
|
|
// Totally different nodes, updating is impossible.
|
|
break;
|
|
}
|
|
|
|
auto newChildNode = progressState(childNode, baseChildNode);
|
|
if (newChildNode) {
|
|
if (!areChildrenChanged) {
|
|
// Making a copy before the first mutation.
|
|
newChildren = children;
|
|
}
|
|
newChildren[index] = newChildNode;
|
|
areChildrenChanged = true;
|
|
}
|
|
}
|
|
|
|
// Stage 2: Misaligned part.
|
|
for (; index < childrenSize; index++) {
|
|
auto newChildNode = progressState(*children.at(index));
|
|
if (newChildNode) {
|
|
if (!areChildrenChanged) {
|
|
// Making a copy before the first mutation.
|
|
newChildren = children;
|
|
}
|
|
newChildren[index] = newChildNode;
|
|
areChildrenChanged = true;
|
|
}
|
|
}
|
|
|
|
if (!areChildrenChanged && !isStateChanged) {
|
|
return nullptr;
|
|
}
|
|
|
|
return shadowNode.clone({
|
|
ShadowNodeFragment::propsPlaceholder(),
|
|
areChildrenChanged ? std::make_shared<ShadowNode::ListOfShared const>(
|
|
std::move(newChildren))
|
|
: ShadowNodeFragment::childrenPlaceholder(),
|
|
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
|
|
});
|
|
}
|
|
|
|
static void updateMountedFlag(
|
|
const SharedShadowNodeList &oldChildren,
|
|
const SharedShadowNodeList &newChildren) {
|
|
// This is a simplified version of Diffing algorithm that only updates
|
|
// `mounted` flag on `ShadowNode`s. The algorithm sets "mounted" flag before
|
|
// "unmounted" to allow `ShadowNode` detect a situation where the node was
|
|
// remounted.
|
|
|
|
if (&oldChildren == &newChildren) {
|
|
// Lists are identical, nothing to do.
|
|
return;
|
|
}
|
|
|
|
if (oldChildren.empty() && newChildren.empty()) {
|
|
// Both lists are empty, nothing to do.
|
|
return;
|
|
}
|
|
|
|
size_t index;
|
|
|
|
// Stage 1: Mount and unmount "updated" children.
|
|
for (index = 0; index < oldChildren.size() && index < newChildren.size();
|
|
index++) {
|
|
const auto &oldChild = oldChildren[index];
|
|
const auto &newChild = newChildren[index];
|
|
|
|
if (oldChild == newChild) {
|
|
// Nodes are identical, skipping the subtree.
|
|
continue;
|
|
}
|
|
|
|
if (!ShadowNode::sameFamily(*oldChild, *newChild)) {
|
|
// Totally different nodes, updating is impossible.
|
|
break;
|
|
}
|
|
|
|
newChild->setMounted(true);
|
|
oldChild->setMounted(false);
|
|
|
|
updateMountedFlag(oldChild->getChildren(), newChild->getChildren());
|
|
}
|
|
|
|
size_t lastIndexAfterFirstStage = index;
|
|
|
|
// State 2: Mount new children.
|
|
for (index = lastIndexAfterFirstStage; index < newChildren.size(); index++) {
|
|
const auto &newChild = newChildren[index];
|
|
newChild->setMounted(true);
|
|
updateMountedFlag({}, newChild->getChildren());
|
|
}
|
|
|
|
// State 3: Unmount old children.
|
|
for (index = lastIndexAfterFirstStage; index < oldChildren.size(); index++) {
|
|
const auto &oldChild = oldChildren[index];
|
|
oldChild->setMounted(false);
|
|
updateMountedFlag(oldChild->getChildren(), {});
|
|
}
|
|
}
|
|
|
|
ShadowTree::ShadowTree(
|
|
SurfaceId surfaceId,
|
|
LayoutConstraints const &layoutConstraints,
|
|
LayoutContext const &layoutContext,
|
|
ShadowTreeDelegate const &delegate,
|
|
ContextContainer const &contextContainer)
|
|
: surfaceId_(surfaceId), delegate_(delegate) {
|
|
const auto noopEventEmitter = std::make_shared<const ViewEventEmitter>(
|
|
nullptr, -1, std::shared_ptr<const EventDispatcher>());
|
|
|
|
static auto globalRootComponentDescriptor =
|
|
std::make_unique<RootComponentDescriptor const>(
|
|
ComponentDescriptorParameters{
|
|
EventDispatcher::Shared{}, nullptr, nullptr});
|
|
|
|
const auto props = std::make_shared<const RootProps>(
|
|
PropsParserContext{surfaceId, contextContainer},
|
|
*RootShadowNode::defaultSharedProps(),
|
|
layoutConstraints,
|
|
layoutContext);
|
|
|
|
auto const fragment =
|
|
ShadowNodeFamilyFragment{surfaceId, surfaceId, noopEventEmitter};
|
|
auto family = globalRootComponentDescriptor->createFamily(fragment, nullptr);
|
|
|
|
auto rootShadowNode = std::static_pointer_cast<const RootShadowNode>(
|
|
globalRootComponentDescriptor->createShadowNode(
|
|
ShadowNodeFragment{
|
|
/* .props = */ props,
|
|
},
|
|
family));
|
|
|
|
currentRevision_ = ShadowTreeRevision{
|
|
rootShadowNode, INITIAL_REVISION, TransactionTelemetry{}};
|
|
|
|
mountingCoordinator_ =
|
|
std::make_shared<MountingCoordinator const>(currentRevision_);
|
|
}
|
|
|
|
ShadowTree::~ShadowTree() {
|
|
mountingCoordinator_->revoke();
|
|
}
|
|
|
|
Tag ShadowTree::getSurfaceId() const {
|
|
return surfaceId_;
|
|
}
|
|
|
|
void ShadowTree::setCommitMode(CommitMode commitMode) const {
|
|
auto revision = ShadowTreeRevision{};
|
|
|
|
{
|
|
std::unique_lock<better::shared_mutex> lock(commitMutex_);
|
|
if (commitMode_ == commitMode) {
|
|
return;
|
|
}
|
|
|
|
commitMode_ = commitMode;
|
|
revision = currentRevision_;
|
|
}
|
|
|
|
// initial revision never contains any commits so mounting it here is
|
|
// incorrect
|
|
if (commitMode == CommitMode::Normal && revision.number != INITIAL_REVISION) {
|
|
mount(revision);
|
|
}
|
|
}
|
|
|
|
CommitMode ShadowTree::getCommitMode() const {
|
|
std::shared_lock<better::shared_mutex> lock(commitMutex_);
|
|
return commitMode_;
|
|
}
|
|
|
|
MountingCoordinator::Shared ShadowTree::getMountingCoordinator() const {
|
|
return mountingCoordinator_;
|
|
}
|
|
|
|
CommitStatus ShadowTree::commit(
|
|
ShadowTreeCommitTransaction transaction,
|
|
CommitOptions commitOptions) const {
|
|
SystraceSection s("ShadowTree::commit");
|
|
|
|
int attempts = 0;
|
|
|
|
while (true) {
|
|
attempts++;
|
|
|
|
auto status = tryCommit(transaction, commitOptions);
|
|
if (status != CommitStatus::Failed) {
|
|
return status;
|
|
}
|
|
|
|
// After multiple attempts, we failed to commit the transaction.
|
|
// Something internally went terribly wrong.
|
|
react_native_assert(attempts < 1024);
|
|
}
|
|
}
|
|
|
|
CommitStatus ShadowTree::tryCommit(
|
|
ShadowTreeCommitTransaction transaction,
|
|
CommitOptions commitOptions) const {
|
|
SystraceSection s("ShadowTree::tryCommit");
|
|
|
|
auto telemetry = TransactionTelemetry{};
|
|
telemetry.willCommit();
|
|
|
|
CommitMode commitMode;
|
|
auto oldRevision = ShadowTreeRevision{};
|
|
auto newRevision = ShadowTreeRevision{};
|
|
|
|
{
|
|
// Reading `currentRevision_` in shared manner.
|
|
std::shared_lock<better::shared_mutex> lock(commitMutex_);
|
|
commitMode = commitMode_;
|
|
oldRevision = currentRevision_;
|
|
}
|
|
|
|
auto oldRootShadowNode = oldRevision.rootShadowNode;
|
|
auto newRootShadowNode = transaction(*oldRevision.rootShadowNode);
|
|
|
|
if (!newRootShadowNode ||
|
|
(commitOptions.shouldYield && commitOptions.shouldYield())) {
|
|
return CommitStatus::Cancelled;
|
|
}
|
|
|
|
if (commitOptions.enableStateReconciliation) {
|
|
auto updatedNewRootShadowNode =
|
|
progressState(*newRootShadowNode, *oldRevision.rootShadowNode);
|
|
if (updatedNewRootShadowNode) {
|
|
newRootShadowNode =
|
|
std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
|
|
}
|
|
}
|
|
|
|
// Layout nodes.
|
|
std::vector<LayoutableShadowNode const *> affectedLayoutableNodes{};
|
|
affectedLayoutableNodes.reserve(1024);
|
|
|
|
telemetry.willLayout();
|
|
telemetry.setAsThreadLocal();
|
|
newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
|
|
telemetry.unsetAsThreadLocal();
|
|
telemetry.didLayout();
|
|
|
|
// Seal the shadow node so it can no longer be mutated
|
|
newRootShadowNode->sealRecursive();
|
|
|
|
{
|
|
// Updating `currentRevision_` in unique manner if it hasn't changed.
|
|
std::unique_lock<better::shared_mutex> lock(commitMutex_);
|
|
|
|
if (currentRevision_.number != oldRevision.number) {
|
|
return CommitStatus::Failed;
|
|
}
|
|
|
|
auto newRevisionNumber = oldRevision.number + 1;
|
|
|
|
newRootShadowNode = delegate_.shadowTreeWillCommit(
|
|
*this, oldRootShadowNode, newRootShadowNode);
|
|
|
|
if (!newRootShadowNode ||
|
|
(commitOptions.shouldYield && commitOptions.shouldYield())) {
|
|
return CommitStatus::Cancelled;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> dispatchLock(EventEmitter::DispatchMutex());
|
|
|
|
updateMountedFlag(
|
|
currentRevision_.rootShadowNode->getChildren(),
|
|
newRootShadowNode->getChildren());
|
|
}
|
|
|
|
telemetry.didCommit();
|
|
telemetry.setRevisionNumber(static_cast<int>(newRevisionNumber));
|
|
|
|
newRevision =
|
|
ShadowTreeRevision{newRootShadowNode, newRevisionNumber, telemetry};
|
|
|
|
currentRevision_ = newRevision;
|
|
}
|
|
|
|
emitLayoutEvents(affectedLayoutableNodes);
|
|
|
|
if (commitMode == CommitMode::Normal) {
|
|
mount(newRevision);
|
|
}
|
|
|
|
return CommitStatus::Succeeded;
|
|
}
|
|
|
|
ShadowTreeRevision ShadowTree::getCurrentRevision() const {
|
|
std::shared_lock<better::shared_mutex> lock(commitMutex_);
|
|
return currentRevision_;
|
|
}
|
|
|
|
void ShadowTree::mount(ShadowTreeRevision const &revision) const {
|
|
mountingCoordinator_->push(revision);
|
|
delegate_.shadowTreeDidFinishTransaction(*this, mountingCoordinator_);
|
|
}
|
|
|
|
void ShadowTree::commitEmptyTree() const {
|
|
commit(
|
|
[](RootShadowNode const &oldRootShadowNode) -> RootShadowNode::Unshared {
|
|
return std::make_shared<RootShadowNode>(
|
|
oldRootShadowNode,
|
|
ShadowNodeFragment{
|
|
/* .props = */ ShadowNodeFragment::propsPlaceholder(),
|
|
/* .children = */ ShadowNode::emptySharedShadowNodeSharedList(),
|
|
});
|
|
});
|
|
}
|
|
|
|
void ShadowTree::emitLayoutEvents(
|
|
std::vector<LayoutableShadowNode const *> &affectedLayoutableNodes) const {
|
|
SystraceSection s("ShadowTree::emitLayoutEvents");
|
|
|
|
for (auto const *layoutableNode : affectedLayoutableNodes) {
|
|
// Only instances of `ViewShadowNode` (and subclasses) are supported.
|
|
auto const &viewShadowNode =
|
|
static_cast<ViewShadowNode const &>(*layoutableNode);
|
|
auto const &viewEventEmitter = static_cast<ViewEventEmitter const &>(
|
|
*viewShadowNode.getEventEmitter());
|
|
|
|
// Checking if the `onLayout` event was requested for the particular Shadow
|
|
// Node.
|
|
auto const &viewProps =
|
|
static_cast<ViewProps const &>(*viewShadowNode.getProps());
|
|
if (!viewProps.onLayout) {
|
|
continue;
|
|
}
|
|
|
|
viewEventEmitter.onLayout(layoutableNode->getLayoutMetrics());
|
|
}
|
|
}
|
|
|
|
void ShadowTree::notifyDelegatesOfUpdates() const {
|
|
delegate_.shadowTreeDidFinishTransaction(*this, mountingCoordinator_);
|
|
}
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|