mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implementation of a subset of MutationObserver (#38100)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38100 This creates an experimental subset of MutationObserver. This will not be available yet and is only compatible with the new React Native architecture. This shouldn't show up in the changelog until we're ready to enable this in some form. Changelog: [Internal] ## Context This implements a subset of the `MutationObserver` API (as defined on the Web) for React Native. This subset allows observing nodes added or removed from the UI tree (at any depth) without modifications in user code. This is not possible at the moment and it's required by some performance logging APIs that rely on observability for tracking. **This is not intended for general use at the moment**. We will continue experimenting with this API and might change important details about how it works. ## Implementation details This API is implemented as a native module that registers a commit hook in Fabric. Whenever there's a commit in React Native, we check for nodes added or removed from the shadow tree (compared with the previous committed version). **This implementation is completely cross-platform.** This API uses a centralized entity in JS and native to handle registration of observers and dispatch of notifications. The dispatch the notifications for all observers in the same callback so we can easily change the sequencing of events easily (for example, we can change this to use microtasks when they're available in RN). An important aspect to highlight about this is that we call into JavaScript when executing the commit hooks. **We assume that commit hooks that trigger mutations will always happen in the JS thread**, so it's safe to do so. This was necessary because React nullifies all fields in fiber when they're unmounted, and we're reporting nodes being deleted. If we wait too long to get a reference to the public instances, they become unavailable and this API doesn't work correctly. To avoid this we eagerly get references to these public instances right when mutations are being applied, so it's safe. ## Known limitations * Only the `childList` and `subtree` options are supported. * There is a feature flag in React Native to do commits in background threads to avoid having to do that work in JS (including layout). This implementation is NOT compatible with that and we're currently working on removing the background executor permanently. * Notifications from `MutationObserver` are dispatched as regular JavaScript tasks, not as microtasks, and they don't block rendering/mount/paint. Some consequences of that are: * UI changes done in MutationObserver callbacks are not flushed atomically with the changes being observed. This is caused by the lack of microtasks and because Fabric doesn't wait for them to mount commits in the host platform. * MutationObserver callbacks cannot reliably set up intersection observers to get a notification for the first paint, as it could have potentially happened already (similar to the previous point). We are working on infrastructure changes (support for microtasks, blocking paint on microtasks, etc.) to overcome this limitations. ---- Reviewed By: sammy-SC Differential Revision: D46149086 fbshipit-source-id: be7ba4acd0886d1661a6500d61d2122f34b98b5a
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2ccde4060e
commit
b1040c4e46
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {polyfillGlobal} from '../Utilities/PolyfillFunctions';
|
||||
|
||||
polyfillGlobal(
|
||||
'MutationObserver',
|
||||
() => require('../MutationObserver/MutationObserver').default,
|
||||
);
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
// flowlint unsafe-getters-setters:off
|
||||
|
||||
import type {MutationObserverId} from './MutationObserverManager';
|
||||
import type MutationRecord from './MutationRecord';
|
||||
|
||||
import ReactNativeElement from '../DOM/Nodes/ReactNativeElement';
|
||||
import * as MutationObserverManager from './MutationObserverManager';
|
||||
|
||||
export type MutationObserverCallback = (
|
||||
mutationRecords: $ReadOnlyArray<MutationRecord>,
|
||||
observer: MutationObserver,
|
||||
) => mixed;
|
||||
|
||||
type MutationObserverInit = $ReadOnly<{
|
||||
subtree?: boolean,
|
||||
// This is the only supported option so it's required to be `true`.
|
||||
childList: true,
|
||||
|
||||
// Unsupported:
|
||||
attributes?: boolean,
|
||||
attributeFilter?: $ReadOnlyArray<string>,
|
||||
attributeOldValue?: boolean,
|
||||
characterData?: boolean,
|
||||
characterDataOldValue?: boolean,
|
||||
}>;
|
||||
|
||||
/**
|
||||
* This is a React Native implementation for the `MutationObserver` API
|
||||
* (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
|
||||
*
|
||||
* It only supports the `subtree` and `childList` options at the moment.
|
||||
*/
|
||||
export default class MutationObserver {
|
||||
_callback: MutationObserverCallback;
|
||||
_observationTargets: Set<ReactNativeElement> = new Set();
|
||||
_mutationObserverId: ?MutationObserverId;
|
||||
|
||||
constructor(callback: MutationObserverCallback): void {
|
||||
if (callback == null) {
|
||||
throw new TypeError(
|
||||
"Failed to construct 'MutationObserver': 1 argument required, but only 0 present.",
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(
|
||||
"Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'.",
|
||||
);
|
||||
}
|
||||
|
||||
this._callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the `MutationObserver` callback to begin receiving notifications
|
||||
* of changes to the UI tree that match the given options.
|
||||
* Depending on the configuration, the observer may watch a single node in the
|
||||
* UI tree, or that node and some or all of its descendant nodes.
|
||||
* To stop the `MutationObserver` (so that none of its callbacks will be
|
||||
* triggered any longer), call `MutationObserver.disconnect()`.
|
||||
*/
|
||||
observe(target: ReactNativeElement, options?: MutationObserverInit): void {
|
||||
if (!(target instanceof ReactNativeElement)) {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'ReactNativeElement'.",
|
||||
);
|
||||
}
|
||||
|
||||
// Browsers force a cast of this value to boolean
|
||||
if (Boolean(options?.childList) !== true) {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'observe' on 'MutationObserver': The options object must set 'childList' to true.",
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.attributes != null) {
|
||||
throw new Error(
|
||||
"Failed to execute 'observe' on 'MutationObserver': attributes is not supported",
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.attributeFilter != null) {
|
||||
throw new Error(
|
||||
"Failed to execute 'observe' on 'MutationObserver': attributeFilter is not supported",
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.attributeOldValue != null) {
|
||||
throw new Error(
|
||||
"Failed to execute 'observe' on 'MutationObserver': attributeOldValue is not supported",
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.characterData != null) {
|
||||
throw new Error(
|
||||
"Failed to execute 'observe' on 'MutationObserver': characterData is not supported",
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.characterDataOldValue != null) {
|
||||
throw new Error(
|
||||
"Failed to execute 'observe' on 'MutationObserver': characterDataOldValue is not supported",
|
||||
);
|
||||
}
|
||||
|
||||
const mutationObserverId = this._getOrCreateMutationObserverId();
|
||||
|
||||
// As per the spec, if the target is already being observed, we "reset"
|
||||
// the observation and only use the last options used.
|
||||
if (this._observationTargets.has(target)) {
|
||||
MutationObserverManager.unobserve(mutationObserverId, target);
|
||||
}
|
||||
|
||||
MutationObserverManager.observe({
|
||||
mutationObserverId,
|
||||
target,
|
||||
subtree: Boolean(options?.subtree),
|
||||
});
|
||||
|
||||
this._observationTargets.add(target);
|
||||
}
|
||||
|
||||
_unobserve(target: ReactNativeElement): void {
|
||||
if (!(target instanceof ReactNativeElement)) {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'ReactNativeElement'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!this._observationTargets.has(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mutationObserverId = this._mutationObserverId;
|
||||
if (mutationObserverId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MutationObserverManager.unobserve(mutationObserverId, target);
|
||||
this._observationTargets.delete(target);
|
||||
|
||||
if (this._observationTargets.size === 0) {
|
||||
MutationObserverManager.unregisterObserver(mutationObserverId);
|
||||
this._mutationObserverId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the observer to stop watching for mutations.
|
||||
* The observer can be reused by calling its `observe()` method again.
|
||||
*/
|
||||
disconnect(): void {
|
||||
for (const target of this._observationTargets.keys()) {
|
||||
this._unobserve(target);
|
||||
}
|
||||
}
|
||||
|
||||
_getOrCreateMutationObserverId(): MutationObserverId {
|
||||
let mutationObserverId = this._mutationObserverId;
|
||||
if (mutationObserverId == null) {
|
||||
mutationObserverId = MutationObserverManager.registerObserver(
|
||||
this,
|
||||
this._callback,
|
||||
);
|
||||
this._mutationObserverId = mutationObserverId;
|
||||
}
|
||||
return mutationObserverId;
|
||||
}
|
||||
|
||||
// Only for tests
|
||||
__getObserverID(): ?MutationObserverId {
|
||||
return this._mutationObserverId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
/**
|
||||
* This module handles the communication between the React Native renderer
|
||||
* and all the mutation observers that are currently observing any targets.
|
||||
*
|
||||
* In order to reduce the communication between native and JavaScript,
|
||||
* we register a single notication callback in native, and then we handle how
|
||||
* to notify each entry to the right mutation observer when we receive all
|
||||
* the notifications together.
|
||||
*/
|
||||
|
||||
import type ReactNativeElement from '../DOM/Nodes/ReactNativeElement';
|
||||
import type MutationObserver, {
|
||||
MutationObserverCallback,
|
||||
} from './MutationObserver';
|
||||
import type MutationRecord from './MutationRecord';
|
||||
|
||||
import {
|
||||
getPublicInstanceFromInternalInstanceHandle,
|
||||
getShadowNode,
|
||||
} from '../DOM/Nodes/ReadOnlyNode';
|
||||
import * as Systrace from '../Performance/Systrace';
|
||||
import warnOnce from '../Utilities/warnOnce';
|
||||
import {createMutationRecord} from './MutationRecord';
|
||||
import NativeMutationObserver from './NativeMutationObserver';
|
||||
|
||||
export type MutationObserverId = number;
|
||||
|
||||
let nextMutationObserverId: MutationObserverId = 1;
|
||||
let isConnected: boolean = false;
|
||||
|
||||
const registeredMutationObservers: Map<
|
||||
MutationObserverId,
|
||||
$ReadOnly<{observer: MutationObserver, callback: MutationObserverCallback}>,
|
||||
> = new Map();
|
||||
|
||||
/**
|
||||
* Registers the given mutation observer and returns a unique ID for it,
|
||||
* which is required to start observing targets.
|
||||
*/
|
||||
export function registerObserver(
|
||||
observer: MutationObserver,
|
||||
callback: MutationObserverCallback,
|
||||
): MutationObserverId {
|
||||
const mutationObserverId = nextMutationObserverId;
|
||||
nextMutationObserverId++;
|
||||
registeredMutationObservers.set(mutationObserverId, {
|
||||
observer,
|
||||
callback,
|
||||
});
|
||||
return mutationObserverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the given mutation observer.
|
||||
* This should only be called when an observer is no longer observing any
|
||||
* targets.
|
||||
*/
|
||||
export function unregisterObserver(
|
||||
mutationObserverId: MutationObserverId,
|
||||
): void {
|
||||
const deleted = registeredMutationObservers.delete(mutationObserverId);
|
||||
if (deleted && registeredMutationObservers.size === 0) {
|
||||
// When there are no observers left, we can disconnect the native module
|
||||
// so we don't need to check commits for mutations.
|
||||
NativeMutationObserver?.disconnect();
|
||||
isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function observe({
|
||||
mutationObserverId,
|
||||
target,
|
||||
subtree,
|
||||
}: {
|
||||
mutationObserverId: MutationObserverId,
|
||||
target: ReactNativeElement,
|
||||
subtree: boolean,
|
||||
}): void {
|
||||
if (NativeMutationObserver == null) {
|
||||
warnNoNativeMutationObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
const registeredObserver =
|
||||
registeredMutationObservers.get(mutationObserverId);
|
||||
if (registeredObserver == null) {
|
||||
console.error(
|
||||
`MutationObserverManager: could not start observing target because MutationObserver with ID ${mutationObserverId} was not registered.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetShadowNode = getShadowNode(target);
|
||||
if (targetShadowNode == null) {
|
||||
console.error(
|
||||
'MutationObserverManager: could not find reference to host node from target',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
NativeMutationObserver.connect(
|
||||
notifyMutationObservers,
|
||||
// We need to do this operation from native to make sure we're retaining
|
||||
// the public instance immediately when mutations occur. Otherwise React
|
||||
// could dereference it in the instance handle and we wouldn't be able to
|
||||
// access it.
|
||||
// $FlowExpectedError[incompatible-call] This is typed as (mixed) => mixed in the native module because the codegen doesn't support the actual types.
|
||||
getPublicInstanceFromInternalInstanceHandle,
|
||||
);
|
||||
isConnected = true;
|
||||
}
|
||||
|
||||
return NativeMutationObserver.observe({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree,
|
||||
});
|
||||
}
|
||||
|
||||
export function unobserve(
|
||||
mutationObserverId: number,
|
||||
target: ReactNativeElement,
|
||||
): void {
|
||||
if (NativeMutationObserver == null) {
|
||||
warnNoNativeMutationObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
const registeredObserver =
|
||||
registeredMutationObservers.get(mutationObserverId);
|
||||
if (registeredObserver == null) {
|
||||
console.error(
|
||||
`MutationObserverManager: could not stop observing target because MutationObserver with ID ${mutationObserverId} was not registered.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetShadowNode = getShadowNode(target);
|
||||
if (targetShadowNode == null) {
|
||||
console.error(
|
||||
'MutationObserverManager: could not find reference to host node from target',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
NativeMutationObserver.unobserve(mutationObserverId, targetShadowNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called from native when there are `MutationObserver`
|
||||
* entries to dispatch.
|
||||
*/
|
||||
function notifyMutationObservers(): void {
|
||||
Systrace.beginEvent('MutationObserverManager.notifyMutationObservers');
|
||||
try {
|
||||
doNotifyMutationObservers();
|
||||
} finally {
|
||||
Systrace.endEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function doNotifyMutationObservers(): void {
|
||||
if (NativeMutationObserver == null) {
|
||||
warnNoNativeMutationObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeRecords = NativeMutationObserver.takeRecords();
|
||||
|
||||
const entriesByObserver: Map<
|
||||
MutationObserverId,
|
||||
Array<MutationRecord>,
|
||||
> = new Map();
|
||||
|
||||
for (const nativeRecord of nativeRecords) {
|
||||
let list = entriesByObserver.get(nativeRecord.mutationObserverId);
|
||||
if (list == null) {
|
||||
list = [];
|
||||
entriesByObserver.set(nativeRecord.mutationObserverId, list);
|
||||
}
|
||||
list.push(createMutationRecord(nativeRecord));
|
||||
}
|
||||
|
||||
for (const [mutationObserverId, entriesForObserver] of entriesByObserver) {
|
||||
const registeredObserver =
|
||||
registeredMutationObservers.get(mutationObserverId);
|
||||
if (!registeredObserver) {
|
||||
// This could happen if the observer is disconnected between commit
|
||||
// and mount. In this case, we can just ignore the entries.
|
||||
return;
|
||||
}
|
||||
|
||||
const {observer, callback} = registeredObserver;
|
||||
try {
|
||||
callback.call(observer, entriesForObserver, observer);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function warnNoNativeMutationObserver() {
|
||||
warnOnce(
|
||||
'missing-native-mutation-observer',
|
||||
'Missing native implementation of MutationObserver',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
// flowlint unsafe-getters-setters:off
|
||||
|
||||
import type ReactNativeElement from '../DOM/Nodes/ReactNativeElement';
|
||||
import type ReadOnlyNode from '../DOM/Nodes/ReadOnlyNode';
|
||||
import type {NativeMutationRecord} from './NativeMutationObserver';
|
||||
|
||||
import NodeList, {createNodeList} from '../DOM/OldStyleCollections/NodeList';
|
||||
|
||||
export type MutationType = 'attributes' | 'characterData' | 'childList';
|
||||
|
||||
/**
|
||||
* The `MutationRecord` is a read-only interface that represents an individual
|
||||
* DOM mutation observed by a `MutationObserver`.
|
||||
*
|
||||
* It is the object inside the array passed to the callback of a `MutationObserver`.
|
||||
*/
|
||||
export default class MutationRecord {
|
||||
_target: ReactNativeElement;
|
||||
_addedNodes: NodeList<ReadOnlyNode>;
|
||||
_removedNodes: NodeList<ReadOnlyNode>;
|
||||
|
||||
constructor(nativeRecord: NativeMutationRecord) {
|
||||
// $FlowExpectedError[incompatible-cast] the codegen doesn't support the actual type.
|
||||
const target = (nativeRecord.target: ReactNativeElement);
|
||||
this._target = target;
|
||||
// $FlowExpectedError[incompatible-cast] the codegen doesn't support the actual type.
|
||||
const addedNodes = (nativeRecord.addedNodes: $ReadOnlyArray<ReadOnlyNode>);
|
||||
this._addedNodes = createNodeList(addedNodes);
|
||||
const removedNodes =
|
||||
// $FlowExpectedError[incompatible-cast] the codegen doesn't support the actual type.
|
||||
(nativeRecord.removedNodes: $ReadOnlyArray<ReadOnlyNode>);
|
||||
this._removedNodes = createNodeList(removedNodes);
|
||||
}
|
||||
|
||||
get addedNodes(): NodeList<ReadOnlyNode> {
|
||||
return this._addedNodes;
|
||||
}
|
||||
|
||||
get attributeName(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
get nextSibling(): ReadOnlyNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
get oldValue(): mixed | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
get previousSibling(): ReadOnlyNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
get removedNodes(): NodeList<ReadOnlyNode> {
|
||||
return this._removedNodes;
|
||||
}
|
||||
|
||||
get target(): ReactNativeElement {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
get type(): MutationType {
|
||||
return 'childList';
|
||||
}
|
||||
}
|
||||
|
||||
export function createMutationRecord(
|
||||
entry: NativeMutationRecord,
|
||||
): MutationRecord {
|
||||
return new MutationRecord(entry);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include "NativeMutationObserver.h"
|
||||
#include <react/renderer/core/ShadowNode.h>
|
||||
#include <react/renderer/debug/SystraceSection.h>
|
||||
#include <react/renderer/uimanager/UIManagerBinding.h>
|
||||
#include <react/renderer/uimanager/primitives.h>
|
||||
|
||||
#include "Plugins.h"
|
||||
|
||||
std::shared_ptr<facebook::react::TurboModule>
|
||||
NativeMutationObserverModuleProvider(
|
||||
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
|
||||
return std::make_shared<facebook::react::NativeMutationObserver>(
|
||||
std::move(jsInvoker));
|
||||
}
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
static UIManager &getUIManagerFromRuntime(jsi::Runtime &runtime) {
|
||||
return UIManagerBinding::getBinding(runtime)->getUIManager();
|
||||
}
|
||||
|
||||
NativeMutationObserver::NativeMutationObserver(
|
||||
std::shared_ptr<CallInvoker> jsInvoker)
|
||||
: NativeMutationObserverCxxSpec(std::move(jsInvoker)) {}
|
||||
|
||||
void NativeMutationObserver::observe(
|
||||
jsi::Runtime &runtime,
|
||||
NativeMutationObserverObserveOptions options) {
|
||||
auto mutationObserverId = options.mutationObserverId;
|
||||
auto subtree = options.subtree;
|
||||
auto shadowNode =
|
||||
shadowNodeFromValue(runtime, std::move(options).targetShadowNode);
|
||||
auto &uiManager = getUIManagerFromRuntime(runtime);
|
||||
|
||||
mutationObserverManager_.observe(
|
||||
mutationObserverId, shadowNode, subtree, uiManager);
|
||||
}
|
||||
|
||||
void NativeMutationObserver::unobserve(
|
||||
jsi::Runtime &runtime,
|
||||
MutationObserverId mutationObserverId,
|
||||
jsi::Object targetShadowNode) {
|
||||
auto shadowNode = shadowNodeFromValue(runtime, std::move(targetShadowNode));
|
||||
mutationObserverManager_.unobserve(mutationObserverId, *shadowNode);
|
||||
}
|
||||
|
||||
void NativeMutationObserver::connect(
|
||||
jsi::Runtime &runtime,
|
||||
AsyncCallback<> notifyMutationObservers,
|
||||
jsi::Function getPublicInstanceFromInstanceHandle) {
|
||||
auto &uiManager = getUIManagerFromRuntime(runtime);
|
||||
|
||||
// MutationObserver is not compatible with background executor.
|
||||
// When using background executor, we commit trees outside the JS thread.
|
||||
// In that case, we can't safely access the JS runtime in commit hooks to
|
||||
// get references to mutated nodes (which we need to do at that point
|
||||
// to ensure we are retaining removed nodes).
|
||||
if (uiManager.hasBackgroundExecutor()) {
|
||||
throw jsi::JSError(
|
||||
runtime,
|
||||
"MutationObserver: could not start observation because MutationObserver is incompatible with UIManager using background executor.");
|
||||
}
|
||||
|
||||
getPublicInstanceFromInstanceHandle_ =
|
||||
jsi::Value(runtime, getPublicInstanceFromInstanceHandle);
|
||||
|
||||
// This should always be called from the JS thread, as it's unsafe to call
|
||||
// into JS otherwise (via `getPublicInstanceFromInstanceHandle`).
|
||||
getPublicInstanceFromShadowNode_ = [&](ShadowNode const &shadowNode) {
|
||||
auto instanceHandle = shadowNode.getInstanceHandle(runtime);
|
||||
if (!instanceHandle.isObject() ||
|
||||
!getPublicInstanceFromInstanceHandle_.isObject() ||
|
||||
!getPublicInstanceFromInstanceHandle_.asObject(runtime).isFunction(
|
||||
runtime)) {
|
||||
return jsi::Value::null();
|
||||
}
|
||||
return getPublicInstanceFromInstanceHandle_.asObject(runtime)
|
||||
.asFunction(runtime)
|
||||
.call(runtime, instanceHandle);
|
||||
};
|
||||
|
||||
notifyMutationObservers_ = std::move(notifyMutationObservers);
|
||||
|
||||
auto onMutationsCallback = [&](std::vector<MutationRecord const> &records) {
|
||||
return onMutations(records);
|
||||
};
|
||||
|
||||
mutationObserverManager_.connect(uiManager, std::move(onMutationsCallback));
|
||||
}
|
||||
|
||||
void NativeMutationObserver::disconnect(jsi::Runtime &runtime) {
|
||||
auto &uiManager = getUIManagerFromRuntime(runtime);
|
||||
mutationObserverManager_.disconnect(uiManager);
|
||||
getPublicInstanceFromInstanceHandle_ = jsi::Value::undefined();
|
||||
getPublicInstanceFromShadowNode_ = nullptr;
|
||||
notifyMutationObservers_ = nullptr;
|
||||
}
|
||||
|
||||
std::vector<NativeMutationRecord> NativeMutationObserver::takeRecords(
|
||||
jsi::Runtime & /*runtime*/) {
|
||||
notifiedMutationObservers_ = false;
|
||||
|
||||
std::vector<NativeMutationRecord> records;
|
||||
pendingRecords_.swap(records);
|
||||
return records;
|
||||
}
|
||||
|
||||
std::vector<jsi::Value>
|
||||
NativeMutationObserver::getPublicInstancesFromShadowNodes(
|
||||
std::vector<ShadowNode::Shared> const &shadowNodes) const {
|
||||
std::vector<jsi::Value> publicInstances;
|
||||
publicInstances.reserve(shadowNodes.size());
|
||||
|
||||
for (auto const &shadowNode : shadowNodes) {
|
||||
publicInstances.push_back(getPublicInstanceFromShadowNode_(*shadowNode));
|
||||
}
|
||||
|
||||
return publicInstances;
|
||||
}
|
||||
|
||||
void NativeMutationObserver::onMutations(
|
||||
std::vector<const MutationRecord> &records) {
|
||||
SystraceSection s("NativeMutationObserver::onMutations");
|
||||
|
||||
for (auto const &record : records) {
|
||||
pendingRecords_.emplace_back(NativeMutationRecord{
|
||||
record.mutationObserverId,
|
||||
// FIXME(T157129303) Instead of assuming we can call into JS from here,
|
||||
// we should use an API that explicitly indicates it.
|
||||
getPublicInstanceFromShadowNode_(*record.targetShadowNode),
|
||||
getPublicInstancesFromShadowNodes(record.addedShadowNodes),
|
||||
getPublicInstancesFromShadowNodes(record.removedShadowNodes)});
|
||||
}
|
||||
|
||||
notifyMutationObserversIfNecessary();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows us to avoid scheduling multiple calls to notify observers
|
||||
* in the JS thread. We schedule one and skip subsequent ones (we just append
|
||||
* the records to the pending list and wait for the scheduled task to consume
|
||||
* all of them).
|
||||
*/
|
||||
void NativeMutationObserver::notifyMutationObserversIfNecessary() {
|
||||
bool dispatchNotification = false;
|
||||
|
||||
if (!pendingRecords_.empty() && !notifiedMutationObservers_) {
|
||||
notifiedMutationObservers_ = true;
|
||||
dispatchNotification = true;
|
||||
}
|
||||
|
||||
if (dispatchNotification) {
|
||||
SystraceSection s("NativeMutationObserver::notifyObservers");
|
||||
notifyMutationObservers_();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace facebook::react
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
|
||||
#include <react/renderer/observers/mutation/MutationObserverManager.h>
|
||||
#include <react/renderer/uimanager/UIManager.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
using NativeMutationObserverObserveOptions =
|
||||
NativeMutationObserverCxxBaseNativeMutationObserverObserveOptions<
|
||||
// mutationObserverId
|
||||
MutationObserverId,
|
||||
// targetShadowNode
|
||||
jsi::Object,
|
||||
// subtree
|
||||
bool>;
|
||||
|
||||
template <>
|
||||
struct Bridging<NativeMutationObserverObserveOptions>
|
||||
: NativeMutationObserverCxxBaseNativeMutationObserverObserveOptionsBridging<
|
||||
// mutationObserverId
|
||||
MutationObserverId,
|
||||
// targetShadowNode
|
||||
jsi::Object,
|
||||
// subtree
|
||||
bool> {};
|
||||
|
||||
using NativeMutationRecord = NativeMutationObserverCxxBaseNativeMutationRecord<
|
||||
// mutationObserverId
|
||||
MutationObserverId,
|
||||
// target
|
||||
jsi::Value,
|
||||
// addedNodes
|
||||
std::vector<jsi::Value>,
|
||||
// removedNodes
|
||||
std::vector<jsi::Value>>;
|
||||
|
||||
template <>
|
||||
struct Bridging<NativeMutationRecord>
|
||||
: NativeMutationObserverCxxBaseNativeMutationRecordBridging<
|
||||
// mutationObserverId
|
||||
MutationObserverId,
|
||||
// target
|
||||
jsi::Value,
|
||||
// addedNodes
|
||||
std::vector<jsi::Value>,
|
||||
// removedNodes
|
||||
std::vector<jsi::Value>> {};
|
||||
|
||||
class NativeMutationObserver
|
||||
: public NativeMutationObserverCxxSpec<NativeMutationObserver>,
|
||||
std::enable_shared_from_this<NativeMutationObserver> {
|
||||
public:
|
||||
NativeMutationObserver(std::shared_ptr<CallInvoker> jsInvoker);
|
||||
|
||||
void observe(
|
||||
jsi::Runtime &runtime,
|
||||
NativeMutationObserverObserveOptions options);
|
||||
|
||||
void unobserve(
|
||||
jsi::Runtime &runtime,
|
||||
MutationObserverId mutationObserverId,
|
||||
jsi::Object targetShadowNode);
|
||||
|
||||
void connect(
|
||||
jsi::Runtime &runtime,
|
||||
AsyncCallback<> notifyMutationObservers,
|
||||
jsi::Function getPublicInstanceFromInstanceHandle);
|
||||
|
||||
void disconnect(jsi::Runtime &runtime);
|
||||
|
||||
std::vector<NativeMutationRecord> takeRecords(jsi::Runtime &runtime);
|
||||
|
||||
private:
|
||||
MutationObserverManager mutationObserverManager_{};
|
||||
|
||||
std::vector<NativeMutationRecord> pendingRecords_;
|
||||
|
||||
// This is passed to `connect` so we can retain references to public instances
|
||||
// when mutation occur, before React cleans up unmounted instances.
|
||||
jsi::Value getPublicInstanceFromInstanceHandle_ = jsi::Value::undefined();
|
||||
std::function<jsi::Value(ShadowNode const &)>
|
||||
getPublicInstanceFromShadowNode_;
|
||||
|
||||
bool notifiedMutationObservers_{};
|
||||
std::function<void()> notifyMutationObservers_;
|
||||
|
||||
void onMutations(std::vector<const MutationRecord> &records);
|
||||
void notifyMutationObserversIfNecessary();
|
||||
|
||||
std::vector<jsi::Value> getPublicInstancesFromShadowNodes(
|
||||
std::vector<ShadowNode::Shared> const &shadowNodes) const;
|
||||
};
|
||||
|
||||
} // namespace facebook::react
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {TurboModule} from '../TurboModule/RCTExport';
|
||||
|
||||
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
|
||||
|
||||
export type MutationObserverId = number;
|
||||
|
||||
// These types are not supported by the codegen.
|
||||
type ShadowNode = mixed;
|
||||
type InstanceHandle = mixed;
|
||||
type ReactNativeElement = mixed;
|
||||
type ReadOnlyNode = mixed;
|
||||
|
||||
export type NativeMutationRecord = {
|
||||
mutationObserverId: MutationObserverId,
|
||||
target: ReactNativeElement,
|
||||
addedNodes: $ReadOnlyArray<ReadOnlyNode>,
|
||||
removedNodes: $ReadOnlyArray<ReadOnlyNode>,
|
||||
...
|
||||
};
|
||||
|
||||
export type NativeMutationObserverObserveOptions = {
|
||||
mutationObserverId: number,
|
||||
targetShadowNode: ShadowNode,
|
||||
subtree: boolean,
|
||||
};
|
||||
|
||||
export interface Spec extends TurboModule {
|
||||
+observe: (options: NativeMutationObserverObserveOptions) => void;
|
||||
+unobserve: (
|
||||
mutationObserverId: number,
|
||||
targetShadowNode: ShadowNode,
|
||||
) => void;
|
||||
+connect: (
|
||||
notifyMutationObservers: () => void,
|
||||
// We need this to retain the public instance before React removes the
|
||||
// reference to it (which happen in mutations that remove nodes, or when
|
||||
// nodes are removed between the change and the callback is executed in JS).
|
||||
getPublicInstanceFromInstanceHandle: (
|
||||
instanceHandle: InstanceHandle,
|
||||
) => ReadOnlyNode,
|
||||
) => void;
|
||||
+disconnect: () => void;
|
||||
+takeRecords: () => $ReadOnlyArray<NativeMutationRecord>;
|
||||
}
|
||||
|
||||
export default (TurboModuleRegistry.get<Spec>(
|
||||
'NativeMutationObserverCxx',
|
||||
): ?Spec);
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a mock of `NativeMutationObserver` implementing the same logic as the
|
||||
* native module and integrating with the existing mock for `FabricUIManager`.
|
||||
* This allows us to test all the JavaScript code for IntersectionObserver in
|
||||
* JavaScript as an integration test using only public APIs.
|
||||
*/
|
||||
|
||||
import type {NodeSet} from '../../ReactNative/FabricUIManager';
|
||||
import type {RootTag} from '../../ReactNative/RootTag';
|
||||
import type {
|
||||
InternalInstanceHandle,
|
||||
Node,
|
||||
} from '../../Renderer/shims/ReactNativeTypes';
|
||||
import type {
|
||||
MutationObserverId,
|
||||
NativeMutationObserverObserveOptions,
|
||||
NativeMutationRecord,
|
||||
Spec,
|
||||
} from '../NativeMutationObserver';
|
||||
|
||||
import ReadOnlyNode from '../../DOM/Nodes/ReadOnlyNode';
|
||||
import {
|
||||
type NodeMock,
|
||||
type UIManagerCommitHook,
|
||||
fromNode,
|
||||
getFabricUIManager,
|
||||
getNodeInChildSet,
|
||||
} from '../../ReactNative/__mocks__/FabricUIManager';
|
||||
import invariant from 'invariant';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
let pendingRecords: Array<NativeMutationRecord> = [];
|
||||
let callback: ?() => void;
|
||||
let getPublicInstance: ?(instanceHandle: InternalInstanceHandle) => mixed;
|
||||
let observersByRootTag: Map<
|
||||
RootTag,
|
||||
Map<MutationObserverId, {deep: Set<Node>, shallow: Set<Node>}>,
|
||||
> = new Map();
|
||||
|
||||
const FabricUIManagerMock = nullthrows(getFabricUIManager());
|
||||
|
||||
function getMockDataFromShadowNode(node: mixed): NodeMock {
|
||||
// $FlowExpectedError[incompatible-call]
|
||||
return fromNode(node);
|
||||
}
|
||||
|
||||
function castToNode(node: mixed): Node {
|
||||
// $FlowExpectedError[incompatible-return]
|
||||
return node;
|
||||
}
|
||||
|
||||
const NativeMutationMock = {
|
||||
observe: (options: NativeMutationObserverObserveOptions): void => {
|
||||
const targetShadowNode = castToNode(options.targetShadowNode);
|
||||
const rootTag = getMockDataFromShadowNode(options.targetShadowNode).rootTag;
|
||||
|
||||
let observers = observersByRootTag.get(rootTag);
|
||||
if (observers == null) {
|
||||
observers = new Map();
|
||||
observersByRootTag.set(rootTag, observers);
|
||||
}
|
||||
let observations = observers.get(options.mutationObserverId);
|
||||
if (observations == null) {
|
||||
observations = {deep: new Set(), shallow: new Set()};
|
||||
observers.set(options.mutationObserverId, observations);
|
||||
}
|
||||
|
||||
const isTargetBeingObserved =
|
||||
observations.deep.has(targetShadowNode) ||
|
||||
observations.shallow.has(targetShadowNode);
|
||||
invariant(!isTargetBeingObserved, 'unexpected duplicate call to observe');
|
||||
|
||||
if (options.subtree) {
|
||||
observations.deep.add(targetShadowNode);
|
||||
} else {
|
||||
observations.shallow.add(targetShadowNode);
|
||||
}
|
||||
},
|
||||
unobserve: (mutationObserverId: number, target: mixed): void => {
|
||||
const targetShadowNode = castToNode(target);
|
||||
|
||||
const observers = observersByRootTag.get(
|
||||
getMockDataFromShadowNode(targetShadowNode).rootTag,
|
||||
);
|
||||
const observations = observers?.get(mutationObserverId);
|
||||
invariant(observations != null, 'unexpected call to unobserve');
|
||||
|
||||
const isTargetBeingObserved =
|
||||
observations.deep.has(targetShadowNode) ||
|
||||
observations.shallow.has(targetShadowNode);
|
||||
invariant(isTargetBeingObserved, 'unexpected call to unobserve');
|
||||
|
||||
observations.deep.delete(targetShadowNode);
|
||||
observations.shallow.delete(targetShadowNode);
|
||||
},
|
||||
connect: (
|
||||
notifyMutationObserversCallback: () => void,
|
||||
getPublicInstanceFromInstanceHandle: (
|
||||
instanceHandle: InternalInstanceHandle,
|
||||
) => mixed,
|
||||
): void => {
|
||||
invariant(callback == null, 'unexpected call to connect');
|
||||
callback = notifyMutationObserversCallback;
|
||||
getPublicInstance = getPublicInstanceFromInstanceHandle;
|
||||
FabricUIManagerMock.__addCommitHook(NativeMutationObserverCommitHook);
|
||||
},
|
||||
disconnect: (): void => {
|
||||
invariant(callback != null, 'unexpected call to disconnect');
|
||||
callback = null;
|
||||
FabricUIManagerMock.__removeCommitHook(NativeMutationObserverCommitHook);
|
||||
},
|
||||
takeRecords: (): $ReadOnlyArray<NativeMutationRecord> => {
|
||||
const currentRecords = pendingRecords;
|
||||
pendingRecords = [];
|
||||
return currentRecords;
|
||||
},
|
||||
};
|
||||
|
||||
(NativeMutationMock: Spec);
|
||||
|
||||
export default NativeMutationMock;
|
||||
|
||||
const NativeMutationObserverCommitHook: UIManagerCommitHook = {
|
||||
shadowTreeWillCommit: (rootTag, oldChildSet, newChildSet) => {
|
||||
runMutationObservations(rootTag, oldChildSet, newChildSet);
|
||||
},
|
||||
};
|
||||
|
||||
function runMutationObservations(
|
||||
rootTag: RootTag,
|
||||
oldChildSet: ?NodeSet,
|
||||
newChildSet: NodeSet,
|
||||
): void {
|
||||
const observers = observersByRootTag.get(rootTag);
|
||||
if (!observers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRecords: Array<NativeMutationRecord> = [];
|
||||
|
||||
for (const [mutationObserverId, observations] of observers) {
|
||||
const processedNodes: Set<Node> = new Set();
|
||||
for (const targetShadowNode of observations.deep) {
|
||||
runMutationObservation({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree: true,
|
||||
oldChildSet,
|
||||
newChildSet,
|
||||
newRecords,
|
||||
processedNodes,
|
||||
});
|
||||
}
|
||||
for (const targetShadowNode of observations.shallow) {
|
||||
runMutationObservation({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree: false,
|
||||
oldChildSet,
|
||||
newChildSet,
|
||||
newRecords,
|
||||
processedNodes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of newRecords) {
|
||||
pendingRecords.push(record);
|
||||
}
|
||||
|
||||
notifyObserversIfNecessary();
|
||||
}
|
||||
|
||||
function findNodeOfSameFamily(list: NodeSet, node: Node): ?Node {
|
||||
for (const current of list) {
|
||||
if (fromNode(current).reactTag === fromNode(node).reactTag) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function recordMutations({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree,
|
||||
oldNode,
|
||||
newNode,
|
||||
newRecords,
|
||||
processedNodes,
|
||||
}: {
|
||||
mutationObserverId: MutationObserverId,
|
||||
targetShadowNode: Node,
|
||||
subtree: boolean,
|
||||
oldNode: Node,
|
||||
newNode: Node,
|
||||
newRecords: Array<NativeMutationRecord>,
|
||||
processedNodes: Set<Node>,
|
||||
}): void {
|
||||
// If the nodes are referentially equal, their children are also the same.
|
||||
if (oldNode === newNode || processedNodes.has(newNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
processedNodes.add(newNode);
|
||||
|
||||
const oldChildren = fromNode(oldNode).children;
|
||||
const newChildren = fromNode(newNode).children;
|
||||
|
||||
const addedNodes = [];
|
||||
const removedNodes = [];
|
||||
|
||||
// Check for removed nodes (and equal nodes for further inspection later)
|
||||
for (const oldChild of oldChildren) {
|
||||
const newChild = findNodeOfSameFamily(newChildren, oldChild);
|
||||
if (newChild == null) {
|
||||
removedNodes.push(oldChild);
|
||||
} else if (subtree) {
|
||||
recordMutations({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree,
|
||||
oldNode: oldChild,
|
||||
newNode: newChild,
|
||||
newRecords,
|
||||
processedNodes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for added nodes
|
||||
for (const newChild of newChildren) {
|
||||
const oldChild = findNodeOfSameFamily(oldChildren, newChild);
|
||||
if (oldChild == null) {
|
||||
addedNodes.push(newChild);
|
||||
}
|
||||
}
|
||||
|
||||
if (addedNodes.length > 0 || removedNodes.length > 0) {
|
||||
newRecords.push({
|
||||
mutationObserverId: mutationObserverId,
|
||||
target: nullthrows(getPublicInstance)(
|
||||
getMockDataFromShadowNode(targetShadowNode).instanceHandle,
|
||||
),
|
||||
addedNodes: addedNodes.map(node => {
|
||||
const readOnlyNode = nullthrows(getPublicInstance)(
|
||||
fromNode(node).instanceHandle,
|
||||
);
|
||||
invariant(
|
||||
readOnlyNode instanceof ReadOnlyNode,
|
||||
'expected instance of ReadOnlyNode',
|
||||
);
|
||||
return readOnlyNode;
|
||||
}),
|
||||
removedNodes: removedNodes.map(node => {
|
||||
const readOnlyNode = nullthrows(getPublicInstance)(
|
||||
fromNode(node).instanceHandle,
|
||||
);
|
||||
invariant(
|
||||
readOnlyNode instanceof ReadOnlyNode,
|
||||
'expected instance of ReadOnlyNode',
|
||||
);
|
||||
return readOnlyNode;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function runMutationObservation({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree,
|
||||
oldChildSet,
|
||||
newChildSet,
|
||||
newRecords,
|
||||
processedNodes,
|
||||
}: {
|
||||
mutationObserverId: MutationObserverId,
|
||||
targetShadowNode: Node,
|
||||
subtree: boolean,
|
||||
oldChildSet: ?NodeSet,
|
||||
newChildSet: NodeSet,
|
||||
newRecords: Array<NativeMutationRecord>,
|
||||
processedNodes: Set<Node>,
|
||||
}): void {
|
||||
if (!oldChildSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldTargetShadowNode = getNodeInChildSet(targetShadowNode, oldChildSet);
|
||||
if (oldTargetShadowNode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTargetShadowNode = getNodeInChildSet(targetShadowNode, newChildSet);
|
||||
if (newTargetShadowNode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordMutations({
|
||||
mutationObserverId,
|
||||
targetShadowNode,
|
||||
subtree,
|
||||
oldNode: oldTargetShadowNode,
|
||||
newNode: newTargetShadowNode,
|
||||
newRecords,
|
||||
processedNodes,
|
||||
});
|
||||
}
|
||||
|
||||
function notifyObserversIfNecessary(): void {
|
||||
if (pendingRecords.length > 0) {
|
||||
// We schedule these using regular tasks in native because microtasks are
|
||||
// still not properly supported.
|
||||
setTimeout(() => callback?.(), 0);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
Spec as FabricUIManager,
|
||||
} from '../FabricUIManager';
|
||||
|
||||
type NodeMock = {
|
||||
export type NodeMock = {
|
||||
children: NodeSet,
|
||||
instanceHandle: InternalInstanceHandle,
|
||||
props: NodeProps,
|
||||
@@ -33,12 +33,12 @@ type NodeMock = {
|
||||
viewName: string,
|
||||
};
|
||||
|
||||
function fromNode(node: Node): NodeMock {
|
||||
export function fromNode(node: Node): NodeMock {
|
||||
// $FlowExpectedError[incompatible-return]
|
||||
return node;
|
||||
}
|
||||
|
||||
function toNode(node: NodeMock): Node {
|
||||
export function toNode(node: NodeMock): Node {
|
||||
// $FlowExpectedError[incompatible-return]
|
||||
return node;
|
||||
}
|
||||
@@ -66,14 +66,10 @@ function ensureHostNode(node: Node): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getAncestorsInCurrentTree(
|
||||
function getAncestorsInChildSet(
|
||||
node: Node,
|
||||
childSet: NodeSet,
|
||||
): ?$ReadOnlyArray<[Node, number]> {
|
||||
const childSet = roots.get(fromNode(node).rootTag);
|
||||
if (childSet == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootNode = toNode({
|
||||
reactTag: 0,
|
||||
rootTag: fromNode(node).rootTag,
|
||||
@@ -96,6 +92,17 @@ function getAncestorsInCurrentTree(
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAncestorsInCurrentTree(
|
||||
node: Node,
|
||||
): ?$ReadOnlyArray<[Node, number]> {
|
||||
const childSet = roots.get(fromNode(node).rootTag);
|
||||
if (childSet == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getAncestorsInChildSet(node, childSet);
|
||||
}
|
||||
|
||||
function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> {
|
||||
if (fromNode(root).reactTag === fromNode(node).reactTag) {
|
||||
return [];
|
||||
@@ -113,8 +120,8 @@ function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNodeInCurrentTree(node: Node): ?Node {
|
||||
const ancestors = getAncestorsInCurrentTree(node);
|
||||
export function getNodeInChildSet(node: Node, childSet: NodeSet): ?Node {
|
||||
const ancestors = getAncestorsInChildSet(node, childSet);
|
||||
if (ancestors == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -124,6 +131,15 @@ function getNodeInCurrentTree(node: Node): ?Node {
|
||||
return nodeInCurrentTree;
|
||||
}
|
||||
|
||||
function getNodeInCurrentTree(node: Node): ?Node {
|
||||
const childSet = roots.get(fromNode(node).rootTag);
|
||||
if (childSet == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getNodeInChildSet(node, childSet);
|
||||
}
|
||||
|
||||
function* dfs(node: ?Node): Iterator<Node> {
|
||||
if (node == null) {
|
||||
return;
|
||||
@@ -145,8 +161,20 @@ function hasDisplayNone(node: Node): boolean {
|
||||
|
||||
interface IFabricUIManagerMock extends FabricUIManager {
|
||||
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle;
|
||||
__addCommitHook(commitHook: UIManagerCommitHook): void;
|
||||
__removeCommitHook(commitHook: UIManagerCommitHook): void;
|
||||
}
|
||||
|
||||
export interface UIManagerCommitHook {
|
||||
shadowTreeWillCommit: (
|
||||
rootTag: RootTag,
|
||||
oldChildSet: ?NodeSet,
|
||||
newChildSet: NodeSet,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const commitHooks: Set<UIManagerCommitHook> = new Set();
|
||||
|
||||
const FabricUIManagerMock: IFabricUIManagerMock = {
|
||||
createNode: jest.fn(
|
||||
(
|
||||
@@ -210,6 +238,9 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
|
||||
childSet.push(child);
|
||||
}),
|
||||
completeRoot: jest.fn((rootTag: RootTag, childSet: NodeSet): void => {
|
||||
commitHooks.forEach(hook =>
|
||||
hook.shadowTreeWillCommit(rootTag, roots.get(rootTag), childSet),
|
||||
);
|
||||
roots.set(rootTag, childSet);
|
||||
}),
|
||||
measure: jest.fn((node: Node, callback: MeasureOnSuccessCallback): void => {
|
||||
@@ -466,6 +497,14 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
|
||||
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle {
|
||||
return fromNode(node).instanceHandle;
|
||||
},
|
||||
|
||||
__addCommitHook(commitHook: UIManagerCommitHook): void {
|
||||
commitHooks.add(commitHook);
|
||||
},
|
||||
|
||||
__removeCommitHook(commitHook: UIManagerCommitHook): void {
|
||||
commitHooks.delete(commitHook);
|
||||
},
|
||||
};
|
||||
|
||||
global.nativeFabricUIManager = FabricUIManagerMock;
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include "MutationObserver.h"
|
||||
#include <react/renderer/core/ShadowNodeTraits.h>
|
||||
#include <react/renderer/uimanager/primitives.h>
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
MutationObserver::MutationObserver(MutationObserverId mutationObserverId)
|
||||
: mutationObserverId_(mutationObserverId) {}
|
||||
|
||||
void MutationObserver::observe(
|
||||
ShadowNode::Shared targetShadowNode,
|
||||
bool observeSubtree) {
|
||||
if (observeSubtree) {
|
||||
deeplyObservedShadowNodes_.push_back(targetShadowNode);
|
||||
} else {
|
||||
shallowlyObservedShadowNodes_.push_back(targetShadowNode);
|
||||
}
|
||||
}
|
||||
|
||||
void MutationObserver::unobserve(ShadowNode const &targetShadowNode) {
|
||||
// We don't know if it's being observed deeply or not, so we need to check
|
||||
// both possibilities.
|
||||
deeplyObservedShadowNodes_.erase(
|
||||
std::remove_if(
|
||||
deeplyObservedShadowNodes_.begin(),
|
||||
deeplyObservedShadowNodes_.end(),
|
||||
[&targetShadowNode](auto shadowNode) {
|
||||
return ShadowNode::sameFamily(*shadowNode, targetShadowNode);
|
||||
}),
|
||||
deeplyObservedShadowNodes_.end());
|
||||
|
||||
shallowlyObservedShadowNodes_.erase(
|
||||
std::remove_if(
|
||||
shallowlyObservedShadowNodes_.begin(),
|
||||
shallowlyObservedShadowNodes_.end(),
|
||||
[&targetShadowNode](auto const shadowNode) {
|
||||
return ShadowNode::sameFamily(*shadowNode, targetShadowNode);
|
||||
}),
|
||||
shallowlyObservedShadowNodes_.end());
|
||||
}
|
||||
|
||||
bool MutationObserver::isObserving() const {
|
||||
return !deeplyObservedShadowNodes_.empty() ||
|
||||
!shallowlyObservedShadowNodes_.empty();
|
||||
}
|
||||
|
||||
static ShadowNode::Shared getShadowNodeInTree(
|
||||
ShadowNode const &shadowNode,
|
||||
ShadowNode const &newTree) {
|
||||
auto ancestors = shadowNode.getFamily().getAncestors(newTree);
|
||||
if (ancestors.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto pair = ancestors.rbegin();
|
||||
return pair->first.get().getChildren().at(pair->second);
|
||||
}
|
||||
|
||||
static ShadowNode::Shared findNodeOfSameFamily(
|
||||
ShadowNode::ListOfShared const &list,
|
||||
ShadowNode const &node) {
|
||||
for (auto ¤t : list) {
|
||||
if (ShadowNode::sameFamily(node, *current)) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MutationObserver::recordMutations(
|
||||
RootShadowNode const &oldRootShadowNode,
|
||||
RootShadowNode const &newRootShadowNode,
|
||||
std::vector<const MutationRecord> &recordedMutations) const {
|
||||
// This tracks the nodes that have already been processed by this observer,
|
||||
// so we avoid unnecessary work and duplicated entries.
|
||||
SetOfShadowNodePointers processedNodes;
|
||||
|
||||
// We go over the deeply observed nodes first to avoid skipping nodes that
|
||||
// have only been checked shallowly.
|
||||
for (auto targetShadowNode : deeplyObservedShadowNodes_) {
|
||||
recordMutationsInTarget(
|
||||
targetShadowNode,
|
||||
oldRootShadowNode,
|
||||
newRootShadowNode,
|
||||
true,
|
||||
recordedMutations,
|
||||
processedNodes);
|
||||
}
|
||||
|
||||
for (auto targetShadowNode : shallowlyObservedShadowNodes_) {
|
||||
recordMutationsInTarget(
|
||||
targetShadowNode,
|
||||
oldRootShadowNode,
|
||||
newRootShadowNode,
|
||||
false,
|
||||
recordedMutations,
|
||||
processedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
void MutationObserver::recordMutationsInTarget(
|
||||
ShadowNode::Shared targetShadowNode,
|
||||
RootShadowNode const &oldRootShadowNode,
|
||||
RootShadowNode const &newRootShadowNode,
|
||||
bool observeSubtree,
|
||||
std::vector<const MutationRecord> &recordedMutations,
|
||||
SetOfShadowNodePointers &processedNodes) const {
|
||||
// If the node isnt't present in the old tree, it's either:
|
||||
// - A new node. In that case, the mutation happened in its parent, not in the
|
||||
// node itself.
|
||||
// - A non-existent node. In that case, there are no new mutations.
|
||||
auto oldTargetShadowNode =
|
||||
getShadowNodeInTree(*targetShadowNode, oldRootShadowNode);
|
||||
if (!oldTargetShadowNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the node isn't present in the new tree (and we didn't return in the
|
||||
// previous check), it means the whole node was removed. In that case we don't
|
||||
// record any mutations in the node itself (maybe in its parent if there are
|
||||
// other observers set up).
|
||||
auto newTargetShadowNode =
|
||||
getShadowNodeInTree(*targetShadowNode, newRootShadowNode);
|
||||
if (!newTargetShadowNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordMutationsInSubtrees(
|
||||
std::move(targetShadowNode),
|
||||
*oldTargetShadowNode,
|
||||
*newTargetShadowNode,
|
||||
observeSubtree,
|
||||
recordedMutations,
|
||||
processedNodes);
|
||||
}
|
||||
|
||||
void MutationObserver::recordMutationsInSubtrees(
|
||||
ShadowNode::Shared targetShadowNode,
|
||||
ShadowNode const &oldNode,
|
||||
ShadowNode const &newNode,
|
||||
bool observeSubtree,
|
||||
std::vector<const MutationRecord> &recordedMutations,
|
||||
SetOfShadowNodePointers processedNodes) const {
|
||||
bool isSameNode = &oldNode == &newNode;
|
||||
// If the nodes are referentially equal, their children are also the same.
|
||||
if (isSameNode || processedNodes.find(&newNode) != processedNodes.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
processedNodes.insert(&newNode);
|
||||
|
||||
auto oldChildren = oldNode.getChildren();
|
||||
auto newChildren = newNode.getChildren();
|
||||
|
||||
std::vector<ShadowNode::Shared> addedNodes;
|
||||
std::vector<ShadowNode::Shared> removedNodes;
|
||||
|
||||
// Check for removed nodes (and equal nodes for further inspection)
|
||||
for (auto &oldChild : oldChildren) {
|
||||
auto newChild = findNodeOfSameFamily(newChildren, *oldChild);
|
||||
if (!newChild) {
|
||||
removedNodes.push_back(oldChild);
|
||||
} else if (observeSubtree) {
|
||||
// Nodes are present in both tress. If `subtree` is set to true,
|
||||
// we continue checking their children.
|
||||
recordMutationsInSubtrees(
|
||||
targetShadowNode,
|
||||
*oldChild,
|
||||
*newChild,
|
||||
observeSubtree,
|
||||
recordedMutations,
|
||||
processedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for added nodes
|
||||
for (auto &newChild : newChildren) {
|
||||
auto oldChild = findNodeOfSameFamily(oldChildren, *newChild);
|
||||
if (!oldChild) {
|
||||
addedNodes.push_back(newChild);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedNodes.empty() || !removedNodes.empty()) {
|
||||
recordedMutations.emplace_back(MutationRecord{
|
||||
mutationObserverId_,
|
||||
targetShadowNode,
|
||||
std::move(addedNodes),
|
||||
std::move(removedNodes)});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace facebook::react
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <react/renderer/components/root/RootShadowNode.h>
|
||||
#include <react/renderer/core/ShadowNode.h>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
using MutationObserverId = int32_t;
|
||||
|
||||
struct MutationRecord {
|
||||
MutationObserverId mutationObserverId;
|
||||
ShadowNode::Shared targetShadowNode;
|
||||
std::vector<ShadowNode::Shared> addedShadowNodes;
|
||||
std::vector<ShadowNode::Shared> removedShadowNodes;
|
||||
};
|
||||
|
||||
class MutationObserver {
|
||||
public:
|
||||
MutationObserver(MutationObserverId intersectionObserverId);
|
||||
|
||||
void observe(ShadowNode::Shared targetShadowNode, bool observeSubtree);
|
||||
void unobserve(ShadowNode const &targetShadowNode);
|
||||
|
||||
bool isObserving() const;
|
||||
|
||||
void recordMutations(
|
||||
RootShadowNode const &oldRootShadowNode,
|
||||
RootShadowNode const &newRootShadowNode,
|
||||
std::vector<const MutationRecord> &recordedMutations) const;
|
||||
|
||||
private:
|
||||
MutationObserverId mutationObserverId_;
|
||||
std::vector<ShadowNode::Shared> deeplyObservedShadowNodes_;
|
||||
std::vector<ShadowNode::Shared> shallowlyObservedShadowNodes_;
|
||||
|
||||
using SetOfShadowNodePointers = std::unordered_set<ShadowNode const *>;
|
||||
|
||||
void recordMutationsInTarget(
|
||||
ShadowNode::Shared targetShadowNode,
|
||||
RootShadowNode const &oldRootShadowNode,
|
||||
RootShadowNode const &newRootShadowNode,
|
||||
bool observeSubtree,
|
||||
std::vector<const MutationRecord> &recordedMutations,
|
||||
SetOfShadowNodePointers &processedNodes) const;
|
||||
|
||||
void recordMutationsInSubtrees(
|
||||
ShadowNode::Shared targetShadowNode,
|
||||
ShadowNode const &oldNode,
|
||||
ShadowNode const &newNode,
|
||||
bool observeSubtree,
|
||||
std::vector<const MutationRecord> &recordedMutations,
|
||||
SetOfShadowNodePointers processedNodes) const;
|
||||
};
|
||||
|
||||
} // namespace facebook::react
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include "MutationObserverManager.h"
|
||||
#include <react/renderer/debug/SystraceSection.h>
|
||||
#include <utility>
|
||||
#include "MutationObserver.h"
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
MutationObserverManager::MutationObserverManager() = default;
|
||||
|
||||
void MutationObserverManager::observe(
|
||||
MutationObserverId mutationObserverId,
|
||||
ShadowNode::Shared shadowNode,
|
||||
bool observeSubtree,
|
||||
UIManager const &uiManager) {
|
||||
SystraceSection s("MutationObserverManager::observe");
|
||||
|
||||
auto surfaceId = shadowNode->getSurfaceId();
|
||||
|
||||
auto &observers = observersBySurfaceId_[surfaceId];
|
||||
|
||||
auto observerIt = observers.find(mutationObserverId);
|
||||
if (observerIt == observers.end()) {
|
||||
auto observer = MutationObserver{mutationObserverId};
|
||||
observer.observe(shadowNode, observeSubtree);
|
||||
observers.insert({mutationObserverId, std::move(observer)});
|
||||
} else {
|
||||
auto observer = observerIt->second;
|
||||
observer.observe(shadowNode, observeSubtree);
|
||||
}
|
||||
}
|
||||
|
||||
void MutationObserverManager::unobserve(
|
||||
MutationObserverId mutationObserverId,
|
||||
ShadowNode const &shadowNode) {
|
||||
SystraceSection s("MutationObserverManager::unobserve");
|
||||
|
||||
auto surfaceId = shadowNode.getSurfaceId();
|
||||
|
||||
auto observersIt = observersBySurfaceId_.find(surfaceId);
|
||||
if (observersIt == observersBySurfaceId_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &observers = observersIt->second;
|
||||
|
||||
auto observerIt = observers.find(mutationObserverId);
|
||||
if (observerIt == observers.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &observer = observerIt->second;
|
||||
|
||||
observer.unobserve(shadowNode);
|
||||
|
||||
if (!observer.isObserving()) {
|
||||
observers.erase(mutationObserverId);
|
||||
}
|
||||
|
||||
if (observers.empty()) {
|
||||
observersBySurfaceId_.erase(surfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
void MutationObserverManager::connect(
|
||||
UIManager &uiManager,
|
||||
std::function<void(std::vector<const MutationRecord> &)> onMutations) {
|
||||
SystraceSection s("MutationObserverManager::connect");
|
||||
|
||||
// Fail-safe in case the caller doesn't guarantee consistency.
|
||||
if (commitHookRegistered_) {
|
||||
return;
|
||||
}
|
||||
|
||||
onMutations_ = onMutations;
|
||||
|
||||
uiManager.registerCommitHook(*this);
|
||||
commitHookRegistered_ = true;
|
||||
}
|
||||
|
||||
void MutationObserverManager::disconnect(UIManager &uiManager) {
|
||||
SystraceSection s("MutationObserverManager::disconnect");
|
||||
|
||||
// Fail-safe in case the caller doesn't guarantee consistency.
|
||||
if (!commitHookRegistered_) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiManager.unregisterCommitHook(*this);
|
||||
|
||||
onMutations_ = nullptr;
|
||||
commitHookRegistered_ = false;
|
||||
}
|
||||
|
||||
void MutationObserverManager::commitHookWasRegistered(
|
||||
UIManager const &uiManager) noexcept {}
|
||||
void MutationObserverManager::commitHookWasUnregistered(
|
||||
UIManager const &uiManager) noexcept {}
|
||||
|
||||
RootShadowNode::Unshared MutationObserverManager::shadowTreeWillCommit(
|
||||
ShadowTree const &shadowTree,
|
||||
RootShadowNode::Shared const &oldRootShadowNode,
|
||||
RootShadowNode::Unshared const &newRootShadowNode) noexcept {
|
||||
runMutationObservations(shadowTree, *oldRootShadowNode, *newRootShadowNode);
|
||||
return newRootShadowNode;
|
||||
}
|
||||
|
||||
void MutationObserverManager::runMutationObservations(
|
||||
ShadowTree const &shadowTree,
|
||||
RootShadowNode const &oldRootShadowNode,
|
||||
RootShadowNode const &newRootShadowNode) {
|
||||
SystraceSection s("MutationObserverManager::runMutationObservations");
|
||||
|
||||
auto surfaceId = shadowTree.getSurfaceId();
|
||||
|
||||
auto observersIt = observersBySurfaceId_.find(surfaceId);
|
||||
if (observersIt == observersBySurfaceId_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<const MutationRecord> mutationRecords;
|
||||
|
||||
auto &observers = observersIt->second;
|
||||
for (const auto &[mutationObserverId, observer] : observers) {
|
||||
observer.recordMutations(
|
||||
oldRootShadowNode, newRootShadowNode, mutationRecords);
|
||||
}
|
||||
|
||||
if (!mutationRecords.empty()) {
|
||||
onMutations_(mutationRecords);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} // namespace facebook::react
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <react/renderer/core/ShadowNode.h>
|
||||
#include <react/renderer/uimanager/UIManager.h>
|
||||
#include <react/renderer/uimanager/UIManagerCommitHook.h>
|
||||
#include <vector>
|
||||
#include "MutationObserver.h"
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
class MutationObserverManager final : public UIManagerCommitHook {
|
||||
public:
|
||||
MutationObserverManager();
|
||||
|
||||
void observe(
|
||||
MutationObserverId mutationObserverId,
|
||||
ShadowNode::Shared shadowNode,
|
||||
bool observeSubtree,
|
||||
UIManager const &uiManager);
|
||||
|
||||
void unobserve(
|
||||
MutationObserverId mutationObserverId,
|
||||
ShadowNode const &shadowNode);
|
||||
|
||||
void connect(
|
||||
UIManager &uiManager,
|
||||
std::function<void(std::vector<const MutationRecord> &)> onMutations);
|
||||
|
||||
void disconnect(UIManager &uiManager);
|
||||
|
||||
#pragma mark - UIManagerCommitHook
|
||||
|
||||
void commitHookWasRegistered(UIManager const &uiManager) noexcept override;
|
||||
void commitHookWasUnregistered(UIManager const &uiManager) noexcept override;
|
||||
|
||||
RootShadowNode::Unshared shadowTreeWillCommit(
|
||||
ShadowTree const &shadowTree,
|
||||
RootShadowNode::Shared const &oldRootShadowNode,
|
||||
RootShadowNode::Unshared const &newRootShadowNode) noexcept override;
|
||||
|
||||
private:
|
||||
std::unordered_map<
|
||||
SurfaceId,
|
||||
std::unordered_map<MutationObserverId, MutationObserver>>
|
||||
observersBySurfaceId_;
|
||||
|
||||
std::function<void(std::vector<const MutationRecord> &)> onMutations_;
|
||||
bool commitHookRegistered_{};
|
||||
|
||||
void runMutationObservations(
|
||||
ShadowTree const &shadowTree,
|
||||
RootShadowNode const &oldRootShadowNode,
|
||||
RootShadowNode const &newRootShadowNode);
|
||||
};
|
||||
|
||||
} // namespace facebook::react
|
||||
@@ -201,6 +201,10 @@ class UIManager final : public ShadowTreeDelegate {
|
||||
|
||||
void reportMount(SurfaceId surfaceId) const;
|
||||
|
||||
bool hasBackgroundExecutor() const {
|
||||
return backgroundExecutor_ != nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class UIManagerBinding;
|
||||
friend class Scheduler;
|
||||
|
||||
Reference in New Issue
Block a user