mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
IntersectionObserver: Migrate js infra to shadow node family (#51148)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51148 Intersection observer should not be holding on to shadow nodes. This diff migrates the javascript infra to instead use families. Changelog: [Internal] Reviewed By: yungsters Differential Revision: D74262804 fbshipit-source-id: cc090be54f7312ce32b853ddf86567bb43e676b8
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a4be563935
commit
e6516f2d7b
+55
-14
@@ -21,6 +21,27 @@ NativeIntersectionObserverModuleProvider(
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
namespace {
|
||||
|
||||
jsi::Object tokenFromShadowNodeFamily(
|
||||
jsi::Runtime& runtime,
|
||||
ShadowNodeFamily::Shared shadowNodeFamily) {
|
||||
jsi::Object obj(runtime);
|
||||
// Need to const_cast since JSI only allows non-const pointees
|
||||
obj.setNativeState(
|
||||
runtime,
|
||||
std::const_pointer_cast<ShadowNodeFamily>(std::move(shadowNodeFamily)));
|
||||
return obj;
|
||||
}
|
||||
|
||||
ShadowNodeFamily::Shared shadowNodeFamilyFromToken(
|
||||
jsi::Runtime& runtime,
|
||||
jsi::Object token) {
|
||||
return token.getNativeState<ShadowNodeFamily>(runtime);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NativeIntersectionObserver::NativeIntersectionObserver(
|
||||
std::shared_ptr<CallInvoker> jsInvoker)
|
||||
: NativeIntersectionObserverCxxSpec(std::move(jsInvoker)) {}
|
||||
@@ -28,19 +49,7 @@ NativeIntersectionObserver::NativeIntersectionObserver(
|
||||
void NativeIntersectionObserver::observe(
|
||||
jsi::Runtime& runtime,
|
||||
NativeIntersectionObserverObserveOptions options) {
|
||||
auto intersectionObserverId = options.intersectionObserverId;
|
||||
auto shadowNode =
|
||||
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
|
||||
auto thresholds = options.thresholds;
|
||||
auto rootThresholds = options.rootThresholds;
|
||||
auto& uiManager = getUIManagerFromRuntime(runtime);
|
||||
|
||||
intersectionObserverManager_.observe(
|
||||
intersectionObserverId,
|
||||
shadowNode->getFamilyShared(),
|
||||
thresholds,
|
||||
rootThresholds,
|
||||
uiManager);
|
||||
observeV2(runtime, std::move(options));
|
||||
}
|
||||
|
||||
void NativeIntersectionObserver::unobserve(
|
||||
@@ -48,8 +57,40 @@ void NativeIntersectionObserver::unobserve(
|
||||
IntersectionObserverObserverId intersectionObserverId,
|
||||
jsi::Object targetShadowNode) {
|
||||
auto shadowNode = shadowNodeFromValue(runtime, std::move(targetShadowNode));
|
||||
auto token =
|
||||
tokenFromShadowNodeFamily(runtime, shadowNode->getFamilyShared());
|
||||
unobserveV2(runtime, intersectionObserverId, std::move(token));
|
||||
}
|
||||
|
||||
jsi::Object NativeIntersectionObserver::observeV2(
|
||||
jsi::Runtime& runtime,
|
||||
NativeIntersectionObserverObserveOptions options) {
|
||||
auto intersectionObserverId = options.intersectionObserverId;
|
||||
auto shadowNode =
|
||||
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
|
||||
auto shadowNodeFamily = shadowNode->getFamilyShared();
|
||||
auto thresholds = options.thresholds;
|
||||
auto rootThresholds = options.rootThresholds;
|
||||
auto& uiManager = getUIManagerFromRuntime(runtime);
|
||||
|
||||
intersectionObserverManager_.observe(
|
||||
intersectionObserverId,
|
||||
shadowNodeFamily,
|
||||
thresholds,
|
||||
rootThresholds,
|
||||
uiManager);
|
||||
|
||||
return tokenFromShadowNodeFamily(runtime, shadowNodeFamily);
|
||||
}
|
||||
|
||||
void NativeIntersectionObserver::unobserveV2(
|
||||
jsi::Runtime& runtime,
|
||||
IntersectionObserverObserverId intersectionObserverId,
|
||||
jsi::Object targetToken) {
|
||||
auto shadowNodeFamily =
|
||||
shadowNodeFamilyFromToken(runtime, std::move(targetToken));
|
||||
intersectionObserverManager_.unobserve(
|
||||
intersectionObserverId, shadowNode->getFamilyShared());
|
||||
intersectionObserverId, shadowNodeFamily);
|
||||
}
|
||||
|
||||
void NativeIntersectionObserver::connect(
|
||||
|
||||
+13
@@ -61,15 +61,28 @@ class NativeIntersectionObserver
|
||||
public:
|
||||
NativeIntersectionObserver(std::shared_ptr<CallInvoker> jsInvoker);
|
||||
|
||||
// TODO(T223605846): Remove legacy observe method
|
||||
[[deprecated("Please use observeV2")]]
|
||||
void observe(
|
||||
jsi::Runtime& runtime,
|
||||
NativeIntersectionObserverObserveOptions options);
|
||||
|
||||
// TODO(T223605846): Remove legacy unobserve method
|
||||
[[deprecated("Please use unobserveV2")]]
|
||||
void unobserve(
|
||||
jsi::Runtime& runtime,
|
||||
IntersectionObserverObserverId intersectionObserverId,
|
||||
jsi::Object targetShadowNode);
|
||||
|
||||
jsi::Object observeV2(
|
||||
jsi::Runtime& runtime,
|
||||
NativeIntersectionObserverObserveOptions options);
|
||||
|
||||
void unobserveV2(
|
||||
jsi::Runtime& runtime,
|
||||
IntersectionObserverObserverId intersectionObserverId,
|
||||
jsi::Object targetToken);
|
||||
|
||||
void connect(
|
||||
jsi::Runtime& runtime,
|
||||
AsyncCallback<> notifyIntersectionObserversCallback);
|
||||
|
||||
@@ -39,7 +39,7 @@ struct ShadowNodeFamilyFragment {
|
||||
* Represents all things that shadow nodes from the same family have in common.
|
||||
* To be used inside `ShadowNode` class *only*.
|
||||
*/
|
||||
class ShadowNodeFamily final {
|
||||
class ShadowNodeFamily final : public jsi::NativeState {
|
||||
public:
|
||||
using Shared = std::shared_ptr<const ShadowNodeFamily>;
|
||||
using Weak = std::weak_ptr<const ShadowNodeFamily>;
|
||||
|
||||
@@ -663,6 +663,16 @@ const definitions: FeatureFlagDefinitions = {
|
||||
},
|
||||
ossReleaseStage: 'none',
|
||||
},
|
||||
utilizeTokensInIntersectionObserver: {
|
||||
defaultValue: true,
|
||||
metadata: {
|
||||
dateAdded: '2025-05-06',
|
||||
description: 'Use tokens in IntersectionObserver vs ShadowNode.',
|
||||
expectedReleaseValue: true,
|
||||
purpose: 'experimentation',
|
||||
},
|
||||
ossReleaseStage: 'none',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @generated SignedSource<<6667fc8e4fdd2db0b54c908155b110cf>>
|
||||
* @generated SignedSource<<17fa5e03fe52ed129cf731bba6e9869c>>
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
|
||||
shouldUseAnimatedObjectForTransform: Getter<boolean>,
|
||||
shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean>,
|
||||
shouldUseSetNativePropsInFabric: Getter<boolean>,
|
||||
utilizeTokensInIntersectionObserver: Getter<boolean>,
|
||||
}>;
|
||||
|
||||
export type ReactNativeFeatureFlagsJsOnlyOverrides = OverridesFor<ReactNativeFeatureFlagsJsOnly>;
|
||||
@@ -155,6 +156,11 @@ export const shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean> = cre
|
||||
*/
|
||||
export const shouldUseSetNativePropsInFabric: Getter<boolean> = createJavaScriptFlagGetter('shouldUseSetNativePropsInFabric', true);
|
||||
|
||||
/**
|
||||
* Use tokens in IntersectionObserver vs ShadowNode.
|
||||
*/
|
||||
export const utilizeTokensInIntersectionObserver: Getter<boolean> = createJavaScriptFlagGetter('utilizeTokensInIntersectionObserver', true);
|
||||
|
||||
/**
|
||||
* Common flag for testing. Do NOT modify.
|
||||
*/
|
||||
|
||||
+2
-2
@@ -7,6 +7,7 @@
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @oncall react_native
|
||||
* @fantom_flags utilizeTokensInIntersectionObserver:true
|
||||
*/
|
||||
|
||||
import 'react-native/Libraries/Core/InitializeCore';
|
||||
@@ -844,8 +845,7 @@ describe('IntersectionObserver', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO (T223234714): Fix memory leak and enable this test.
|
||||
it.skip('should not retain initial children of observed targets', () => {
|
||||
it('should not retain initial children of observed targets', () => {
|
||||
const root = Fantom.createRoot();
|
||||
observer = new IntersectionObserver(() => {});
|
||||
|
||||
|
||||
+68
-19
@@ -23,9 +23,11 @@ import type IntersectionObserver, {
|
||||
IntersectionObserverCallback,
|
||||
} from '../IntersectionObserver';
|
||||
import type IntersectionObserverEntry from '../IntersectionObserverEntry';
|
||||
import type {NativeIntersectionObserverToken} from '../specs/NativeIntersectionObserver';
|
||||
|
||||
import * as Systrace from '../../../../../Libraries/Performance/Systrace';
|
||||
import warnOnce from '../../../../../Libraries/Utilities/warnOnce';
|
||||
import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags';
|
||||
import {
|
||||
getInstanceHandle,
|
||||
getNativeNodeReference,
|
||||
@@ -74,6 +76,26 @@ const targetToShadowNodeMap: WeakMap<
|
||||
ReturnType<typeof getNativeNodeReference>,
|
||||
> = new WeakMap();
|
||||
|
||||
const targetToTokenMap: WeakMap<
|
||||
ReactNativeElement,
|
||||
NativeIntersectionObserverToken,
|
||||
> = new WeakMap();
|
||||
|
||||
let modernNativeIntersectionObserver =
|
||||
NativeIntersectionObserver == null
|
||||
? null
|
||||
: NativeIntersectionObserver.observeV2 == null ||
|
||||
NativeIntersectionObserver.unobserveV2 == null
|
||||
? null
|
||||
: {
|
||||
observe: NativeIntersectionObserver.observeV2,
|
||||
unobserve: NativeIntersectionObserver.unobserveV2,
|
||||
};
|
||||
|
||||
if (!ReactNativeFeatureFlags.utilizeTokensInIntersectionObserver()) {
|
||||
modernNativeIntersectionObserver = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given intersection observer and returns a unique ID for it,
|
||||
* which is required to start observing targets.
|
||||
@@ -153,20 +175,32 @@ export function observe({
|
||||
// access it even after the instance handle has been unmounted.
|
||||
setTargetForInstanceHandle(instanceHandle, target);
|
||||
|
||||
// Same for the mapping between the target and its shadow node.
|
||||
targetToShadowNodeMap.set(target, targetNativeNodeReference);
|
||||
if (modernNativeIntersectionObserver == null) {
|
||||
// Same for the mapping between the target and its shadow node.
|
||||
targetToShadowNodeMap.set(target, targetNativeNodeReference);
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
NativeIntersectionObserver.connect(notifyIntersectionObservers);
|
||||
isConnected = true;
|
||||
}
|
||||
|
||||
NativeIntersectionObserver.observe({
|
||||
intersectionObserverId,
|
||||
targetShadowNode: targetNativeNodeReference,
|
||||
thresholds: registeredObserver.observer.thresholds,
|
||||
rootThresholds: registeredObserver.observer.rnRootThresholds,
|
||||
});
|
||||
if (modernNativeIntersectionObserver == null) {
|
||||
NativeIntersectionObserver.observe({
|
||||
intersectionObserverId,
|
||||
targetShadowNode: targetNativeNodeReference,
|
||||
thresholds: registeredObserver.observer.thresholds,
|
||||
rootThresholds: registeredObserver.observer.rnRootThresholds,
|
||||
});
|
||||
} else {
|
||||
const token = modernNativeIntersectionObserver.observe({
|
||||
intersectionObserverId,
|
||||
targetShadowNode: targetNativeNodeReference,
|
||||
thresholds: registeredObserver.observer.thresholds,
|
||||
rootThresholds: registeredObserver.observer.rnRootThresholds,
|
||||
});
|
||||
targetToTokenMap.set(target, token);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -190,18 +224,33 @@ export function unobserve(
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNativeNodeReference = targetToShadowNodeMap.get(target);
|
||||
if (targetNativeNodeReference == null) {
|
||||
console.error(
|
||||
'IntersectionObserverManager: could not find registration data for target',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (modernNativeIntersectionObserver == null) {
|
||||
const targetNativeNodeReference = targetToShadowNodeMap.get(target);
|
||||
if (targetNativeNodeReference == null) {
|
||||
console.error(
|
||||
'IntersectionObserverManager: could not find registration data for target',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
NativeIntersectionObserver.unobserve(
|
||||
intersectionObserverId,
|
||||
targetNativeNodeReference,
|
||||
);
|
||||
NativeIntersectionObserver.unobserve(
|
||||
intersectionObserverId,
|
||||
targetNativeNodeReference,
|
||||
);
|
||||
} else {
|
||||
const targetToken = targetToTokenMap.get(target);
|
||||
if (targetToken == null) {
|
||||
console.error(
|
||||
'IntersectionObserverManager: could not find registration data for target',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
modernNativeIntersectionObserver.unobserve(
|
||||
intersectionObserverId,
|
||||
targetToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+11
@@ -30,9 +30,20 @@ export type NativeIntersectionObserverObserveOptions = {
|
||||
rootThresholds?: ?$ReadOnlyArray<number>,
|
||||
};
|
||||
|
||||
export type NativeIntersectionObserverToken = mixed;
|
||||
|
||||
export interface Spec extends TurboModule {
|
||||
// TODO(T223605846): Remove legacy observe method
|
||||
+observe: (options: NativeIntersectionObserverObserveOptions) => void;
|
||||
// TODO(T223605846): Remove legacy unobserve method
|
||||
+unobserve: (intersectionObserverId: number, targetShadowNode: mixed) => void;
|
||||
+observeV2?: (
|
||||
options: NativeIntersectionObserverObserveOptions,
|
||||
) => NativeIntersectionObserverToken;
|
||||
+unobserveV2?: (
|
||||
intersectionObserverId: number,
|
||||
token: NativeIntersectionObserverToken,
|
||||
) => void;
|
||||
+connect: (notifyIntersectionObserversCallback: () => void) => void;
|
||||
+disconnect: () => void;
|
||||
+takeRecords: () => $ReadOnlyArray<NativeIntersectionObserverEntry>;
|
||||
|
||||
Reference in New Issue
Block a user