main
js 413 lines 13.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 import type {ReactScopeInstance} from 'shared/ReactTypes';
12 import type {
13 ReactDOMEventHandle,
14 ReactDOMEventHandleListener,
15 } from './ReactDOMEventHandleTypes';
16 import type {
17 Container,
18 TextInstance,
19 Instance,
20 ActivityInstance,
21 SuspenseInstance,
22 Props,
23 HoistableRoot,
24 RootResources,
25 } from './ReactFiberConfigDOM';
26
27 import {
28 HostComponent,
29 HostHoistable,
30 HostSingleton,
31 HostText,
32 HostRoot,
33 SuspenseComponent,
34 ActivityComponent,
35 } from 'react-reconciler/src/ReactWorkTags';
36
37 import {getParentHydrationBoundary} from './ReactFiberConfigDOM';
38
39 import {enableScopeAPI} from 'shared/ReactFeatureFlags';
40
41 import {enableInternalInstanceMap} from 'shared/ReactFeatureFlags';
42
43 const randomKey = Math.random().toString(36).slice(2);
44 const internalInstanceKey = '__reactFiber$' + randomKey;
45 const internalPropsKey = '__reactProps$' + randomKey;
46 const internalContainerInstanceKey = '__reactContainer$' + randomKey;
47 const internalEventHandlersKey = '__reactEvents$' + randomKey;
48 const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
49 const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
50 const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
51 const internalHoistableMarker = '__reactMarker$' + randomKey;
52 const internalScrollTimer = '__reactScroll$' + randomKey;
53 const internalLoadPendingKey = '__reactLoad$' + randomKey;
54
55 type InstanceUnion =
56 | Instance
57 | TextInstance
58 | SuspenseInstance
59 | ActivityInstance
60 | ReactScopeInstance
61 | Container;
62
63 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
64 const internalInstanceMap:
65 | WeakMap<InstanceUnion, Fiber>
66 | Map<InstanceUnion, Fiber> = new PossiblyWeakMap();
67 const internalPropsMap:
68 | WeakMap<InstanceUnion, Props>
69 | Map<InstanceUnion, Props> = new PossiblyWeakMap();
70
71 export function detachDeletedInstance(node: Instance): void {
72 // Don't delete the event listener set. The native event listeners it tracks
73 // remain attached to the node, so this bookkeeping needs to last for the
74 // lifetime of the node to prevent duplicate listeners if it is reused.
75 if (enableInternalInstanceMap) {
76 internalInstanceMap.delete(node);
77 internalPropsMap.delete(node);
78 delete (node as any)[internalEventHandlerListenersKey];
79 delete (node as any)[internalEventHandlesSetKey];
80 delete (node as any)[internalRootNodeResourcesKey];
81 if (__DEV__) {
82 delete (node as any)[internalInstanceKey];
83 }
84 return;
85 }
86 // TODO: This function is only called on host components. I don't think all of
87 // these fields are relevant.
88 delete (node as any)[internalInstanceKey];
89 delete (node as any)[internalPropsKey];
90 delete (node as any)[internalEventHandlerListenersKey];
91 delete (node as any)[internalEventHandlesSetKey];
92 }
93
94 export function precacheFiberNode(
95 hostInst: Fiber,
96 node:
97 | Instance
98 | TextInstance
99 | SuspenseInstance
100 | ActivityInstance
101 | ReactScopeInstance,
102 ): void {
103 if (enableInternalInstanceMap) {
104 internalInstanceMap.set(node, hostInst);
105 if (__DEV__) {
106 (node as any)[internalInstanceKey] = hostInst;
107 }
108 return;
109 }
110 (node as any)[internalInstanceKey] = hostInst;
111 }
112
113 export function markContainerAsRoot(hostRoot: Fiber, node: Container): void {
114 // $FlowFixMe[prop-missing]
115 node[internalContainerInstanceKey] = hostRoot;
116 }
117
118 export function unmarkContainerAsRoot(node: Container): void {
119 // $FlowFixMe[prop-missing]
120 node[internalContainerInstanceKey] = null;
121 }
122
123 export function isContainerMarkedAsRoot(node: Container): boolean {
124 // $FlowFixMe[prop-missing]
125 return !!node[internalContainerInstanceKey];
126 }
127
128 // Given a DOM node, return the closest HostComponent or HostText fiber ancestor.
129 // If the target node is part of a hydrated or not yet rendered subtree, then
130 // this may also return a SuspenseComponent, ActivityComponent or HostRoot to
131 // indicate that.
132 // Conceptually the HostRoot fiber is a child of the Container node. So if you
133 // pass the Container node as the targetNode, you will not actually get the
134 // HostRoot back. To get to the HostRoot, you need to pass a child of it.
135 // The same thing applies to Suspense and Activity boundaries.
136 export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
137 let targetInst: void | Fiber;
138 if (enableInternalInstanceMap) {
139 targetInst = internalInstanceMap.get(targetNode as any as InstanceUnion);
140 } else {
141 targetInst = (targetNode as any)[internalInstanceKey];
142 }
143 if (targetInst) {
144 // Don't return HostRoot, SuspenseComponent or ActivityComponent here.
145 return targetInst;
146 }
147 // If the direct event target isn't a React owned DOM node, we need to look
148 // to see if one of its parents is a React owned DOM node.
149 let parentNode = targetNode.parentNode;
150 while (parentNode) {
151 // We'll check if this is a container root that could include
152 // React nodes in the future. We need to check this first because
153 // if we're a child of a dehydrated container, we need to first
154 // find that inner container before moving on to finding the parent
155 // instance. Note that we don't check this field on the targetNode
156 // itself because the fibers are conceptually between the container
157 // node and the first child. It isn't surrounding the container node.
158 // If it's not a container, we check if it's an instance.
159 if (enableInternalInstanceMap) {
160 targetInst =
161 (parentNode as any)[internalContainerInstanceKey] ||
162 internalInstanceMap.get(parentNode as any as InstanceUnion);
163 } else {
164 targetInst =
165 (parentNode as any)[internalContainerInstanceKey] ||
166 (parentNode as any)[internalInstanceKey];
167 }
168 if (targetInst) {
169 // Since this wasn't the direct target of the event, we might have
170 // stepped past dehydrated DOM nodes to get here. However they could
171 // also have been non-React nodes. We need to answer which one.
172
173 // If we the instance doesn't have any children, then there can't be
174 // a nested suspense boundary within it. So we can use this as a fast
175 // bailout. Most of the time, when people add non-React children to
176 // the tree, it is using a ref to a child-less DOM node.
177 // Normally we'd only need to check one of the fibers because if it
178 // has ever gone from having children to deleting them or vice versa
179 // it would have deleted the dehydrated boundary nested inside already.
180 // However, since the HostRoot starts out with an alternate it might
181 // have one on the alternate so we need to check in case this was a
182 // root.
183 const alternate = targetInst.alternate;
184 if (
185 targetInst.child !== null ||
186 (alternate !== null && alternate.child !== null)
187 ) {
188 // Next we need to figure out if the node that skipped past is
189 // nested within a dehydrated boundary and if so, which one.
190 let hydrationInstance = getParentHydrationBoundary(targetNode);
191 while (hydrationInstance !== null) {
192 // We found a suspense instance. That means that we haven't
193 // hydrated it yet. Even though we leave the comments in the
194 // DOM after hydrating, and there are boundaries in the DOM
195 // that could already be hydrated, we wouldn't have found them
196 // through this pass since if the target is hydrated it would
197 // have had an internalInstanceKey on it.
198 // Let's get the fiber associated with the SuspenseComponent
199 // as the deepest instance.
200 const targetFiber = enableInternalInstanceMap
201 ? internalInstanceMap.get(hydrationInstance)
202 : // $FlowFixMe[prop-missing]
203 hydrationInstance[internalInstanceKey];
204 if (targetFiber) {
205 return targetFiber;
206 }
207 // If we don't find a Fiber on the comment, it might be because
208 // we haven't gotten to hydrate it yet. There might still be a
209 // parent boundary that hasn't above this one so we need to find
210 // the outer most that is known.
211 hydrationInstance = getParentHydrationBoundary(hydrationInstance);
212 // If we don't find one, then that should mean that the parent
213 // host component also hasn't hydrated yet. We can return it
214 // below since it will bail out on the isMounted check later.
215 }
216 }
217 return targetInst;
218 }
219 targetNode = parentNode;
220 parentNode = targetNode.parentNode;
221 }
222 return null;
223 }
224
225 /**
226 * Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent
227 * instance, or null if the node was not rendered by this React.
228 */
229 export function getInstanceFromNode(node: Node): Fiber | null {
230 let inst: void | null | Fiber;
231 if (enableInternalInstanceMap) {
232 inst =
233 internalInstanceMap.get(node as any as InstanceUnion) ||
234 (node as any)[internalContainerInstanceKey];
235 } else {
236 inst =
237 (node as any)[internalInstanceKey] ||
238 (node as any)[internalContainerInstanceKey];
239 }
240 if (inst) {
241 const tag = inst.tag;
242 if (
243 tag === HostComponent ||
244 tag === HostText ||
245 tag === SuspenseComponent ||
246 tag === ActivityComponent ||
247 tag === HostHoistable ||
248 tag === HostSingleton ||
249 tag === HostRoot
250 ) {
251 return inst;
252 } else {
253 return null;
254 }
255 }
256 return null;
257 }
258
259 /**
260 * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding
261 * DOM node.
262 */
263 export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
264 const tag = inst.tag;
265 if (
266 tag === HostComponent ||
267 tag === HostHoistable ||
268 tag === HostSingleton ||
269 tag === HostText
270 ) {
271 // In Fiber this, is just the state node right now. We assume it will be
272 // a host component or host text.
273 return inst.stateNode;
274 }
275
276 // Without this first invariant, passing a non-DOM-component triggers the next
277 // invariant for a missing parent, which is super confusing.
278 throw new Error('getNodeFromInstance: Invalid argument.');
279 }
280
281 export function getFiberCurrentPropsFromNode(
282 node:
283 | Container
284 | Instance
285 | TextInstance
286 | SuspenseInstance
287 | ActivityInstance,
288 ): Props | null {
289 if (enableInternalInstanceMap) {
290 return internalPropsMap.get(node) || null;
291 }
292 return (node as any)[internalPropsKey] || null;
293 }
294
295 export function updateFiberProps(node: Instance, props: Props): void {
296 if (enableInternalInstanceMap) {
297 internalPropsMap.set(node, props);
298 return;
299 }
300 (node as any)[internalPropsKey] = props;
301 }
302
303 export function getEventListenerSet(node: EventTarget): Set<string> {
304 let elementListenerSet: Set<string> | void = (node as any)[
305 internalEventHandlersKey
306 ];
307 if (elementListenerSet === undefined) {
308 elementListenerSet = (node as any)[internalEventHandlersKey] = new Set();
309 }
310 return elementListenerSet;
311 }
312
313 export function getFiberFromScopeInstance(
314 scope: ReactScopeInstance,
315 ): null | Fiber {
316 if (enableScopeAPI) {
317 if (enableInternalInstanceMap) {
318 return internalInstanceMap.get(scope as any as InstanceUnion) || null;
319 }
320 return (scope as any)[internalInstanceKey] || null;
321 }
322 return null;
323 }
324
325 export function setEventHandlerListeners(
326 scope: EventTarget | ReactScopeInstance,
327 listeners: Set<ReactDOMEventHandleListener>,
328 ): void {
329 (scope as any)[internalEventHandlerListenersKey] = listeners;
330 }
331
332 export function getEventHandlerListeners(
333 scope: EventTarget | ReactScopeInstance,
334 ): null | Set<ReactDOMEventHandleListener> {
335 return (scope as any)[internalEventHandlerListenersKey] || null;
336 }
337
338 export function addEventHandleToTarget(
339 target: EventTarget | ReactScopeInstance,
340 eventHandle: ReactDOMEventHandle,
341 ): void {
342 let eventHandles = (target as any)[internalEventHandlesSetKey];
343 if (eventHandles === undefined) {
344 eventHandles = (target as any)[internalEventHandlesSetKey] = new Set();
345 }
346 eventHandles.add(eventHandle);
347 }
348
349 export function doesTargetHaveEventHandle(
350 target: EventTarget | ReactScopeInstance,
351 eventHandle: ReactDOMEventHandle,
352 ): boolean {
353 const eventHandles = (target as any)[internalEventHandlesSetKey];
354 if (eventHandles === undefined) {
355 return false;
356 }
357 return eventHandles.has(eventHandle);
358 }
359
360 export function getResourcesFromRoot(root: HoistableRoot): RootResources {
361 let resources = (root as any)[internalRootNodeResourcesKey];
362 if (!resources) {
363 resources = (root as any)[internalRootNodeResourcesKey] = {
364 hoistableStyles: new Map(),
365 hoistableScripts: new Map(),
366 };
367 }
368 return resources;
369 }
370
371 export function isMarkedHoistable(node: Node): boolean {
372 return !!(node as any)[internalHoistableMarker];
373 }
374
375 export function markNodeAsHoistable(node: Node) {
376 (node as any)[internalHoistableMarker] = true;
377 }
378
379 export function getScrollEndTimer(node: EventTarget): ?TimeoutID {
380 return (node as any)[internalScrollTimer];
381 }
382
383 export function setScrollEndTimer(node: EventTarget, timer: TimeoutID): void {
384 (node as any)[internalScrollTimer] = timer;
385 }
386
387 export function clearScrollEndTimer(node: EventTarget): void {
388 (node as any)[internalScrollTimer] = undefined;
389 }
390
391 export function markNodeAsPendingLoad(node: Node): void {
392 (node as any)[internalLoadPendingKey] = true;
393 }
394
395 export function clearPendingLoadOnNode(node: Node): void {
396 (node as any)[internalLoadPendingKey] = undefined;
397 }
398
399 export function isNodePendingLoad(node: Node): boolean {
400 return (node as any)[internalLoadPendingKey] === true;
401 }
402
403 export function isOwnedInstance(node: Node): boolean {
404 if (enableInternalInstanceMap) {
405 return !!(
406 (node as any)[internalHoistableMarker] ||
407 internalInstanceMap.has(node as any)
408 );
409 }
410 return !!(
411 (node as any)[internalHoistableMarker] || (node as any)[internalInstanceKey]
412 );
413 }