main
js 772 lines 19.2 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 './ReactInternalTypes';
11 import type {
12 Container,
13 Instance,
14 TextInstance,
15 ActivityInstance,
16 SuspenseInstance,
17 } from './ReactFiberConfig';
18 import type {ActivityState} from './ReactFiberActivityComponent';
19 import type {SuspenseState} from './ReactFiberSuspenseComponent';
20
21 import {
22 HostComponent,
23 HostHoistable,
24 HostSingleton,
25 HostRoot,
26 HostPortal,
27 HostText,
28 ActivityComponent,
29 SuspenseComponent,
30 OffscreenComponent,
31 Fragment,
32 } from './ReactWorkTags';
33 import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
34 import {enableFragmentRefsTextNodes} from 'shared/ReactFeatureFlags';
35
36 export function getNearestMountedFiber(fiber: Fiber): null | Fiber {
37 let node = fiber;
38 let nearestMounted: null | Fiber = fiber;
39 // If there is no alternate, this might be a new tree that isn't inserted
40 // yet. If it is, then it will have a pending insertion effect on it.
41 let nextNode: Fiber = node;
42 while (nextNode && !nextNode.alternate) {
43 node = nextNode;
44 if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
45 // This is an insertion or in-progress hydration. The nearest possible
46 // mounted fiber is the parent but we need to continue to figure out
47 // if that one is still mounted.
48 nearestMounted = node.return;
49 }
50 // $FlowFixMe[incompatible-type] we bail out when we get a null
51 nextNode = node.return;
52 }
53 // After we've reached an alternate, go the rest of the way to see if the
54 // tree is still mounted. If it's not, its return pointer will be disconnected.
55 while (node.return) {
56 node = node.return;
57 }
58 if (node.tag === HostRoot) {
59 // TODO: Check if this was a nested HostRoot when used with
60 // renderContainerIntoSubtree.
61 return nearestMounted;
62 }
63 // If we didn't hit the root, that means that we're in an disconnected tree
64 // that has been unmounted.
65 return null;
66 }
67
68 export function getSuspenseInstanceFromFiber(
69 fiber: Fiber,
70 ): null | SuspenseInstance {
71 if (fiber.tag === SuspenseComponent) {
72 let suspenseState: SuspenseState | null = fiber.memoizedState;
73 if (suspenseState === null) {
74 const current = fiber.alternate;
75 if (current !== null) {
76 suspenseState = current.memoizedState;
77 }
78 }
79 if (suspenseState !== null) {
80 return suspenseState.dehydrated;
81 }
82 }
83 return null;
84 }
85
86 export function getActivityInstanceFromFiber(
87 fiber: Fiber,
88 ): null | ActivityInstance {
89 if (fiber.tag === ActivityComponent) {
90 let activityState: ActivityState | null = fiber.memoizedState;
91 if (activityState === null) {
92 const current = fiber.alternate;
93 if (current !== null) {
94 activityState = current.memoizedState;
95 }
96 }
97 if (activityState !== null) {
98 return activityState.dehydrated;
99 }
100 }
101 // TODO: Implement this on ActivityComponent.
102 return null;
103 }
104
105 export function getContainerFromFiber(fiber: Fiber): null | Container {
106 return fiber.tag === HostRoot
107 ? (fiber.stateNode.containerInfo as Container)
108 : null;
109 }
110
111 function assertIsMounted(fiber: Fiber) {
112 if (getNearestMountedFiber(fiber) !== fiber) {
113 throw new Error('Unable to find node on an unmounted component.');
114 }
115 }
116
117 export function findCurrentFiberUsingSlowPath(fiber: Fiber): Fiber | null {
118 const alternate = fiber.alternate;
119 if (!alternate) {
120 // If there is no alternate, then we only need to check if it is mounted.
121 const nearestMounted = getNearestMountedFiber(fiber);
122
123 if (nearestMounted === null) {
124 throw new Error('Unable to find node on an unmounted component.');
125 }
126
127 if (nearestMounted !== fiber) {
128 return null;
129 }
130 return fiber;
131 }
132 // If we have two possible branches, we'll walk backwards up to the root
133 // to see what path the root points to. On the way we may hit one of the
134 // special cases and we'll deal with them.
135 let a: Fiber = fiber;
136 let b: Fiber = alternate;
137 while (true) {
138 const parentA = a.return;
139 if (parentA === null) {
140 // We're at the root.
141 break;
142 }
143 const parentB = parentA.alternate;
144 if (parentB === null) {
145 // There is no alternate. This is an unusual case. Currently, it only
146 // happens when a Suspense component is hidden. An extra fragment fiber
147 // is inserted in between the Suspense fiber and its children. Skip
148 // over this extra fragment fiber and proceed to the next parent.
149 const nextParent = parentA.return;
150 if (nextParent !== null) {
151 a = b = nextParent;
152 continue;
153 }
154 // If there's no parent, we're at the root.
155 break;
156 }
157
158 // If both copies of the parent fiber point to the same child, we can
159 // assume that the child is current. This happens when we bailout on low
160 // priority: the bailed out fiber's child reuses the current child.
161 if (parentA.child === parentB.child) {
162 let child = parentA.child;
163 while (child) {
164 if (child === a) {
165 // We've determined that A is the current branch.
166 assertIsMounted(parentA);
167 return fiber;
168 }
169 if (child === b) {
170 // We've determined that B is the current branch.
171 assertIsMounted(parentA);
172 return alternate;
173 }
174 child = child.sibling;
175 }
176
177 // We should never have an alternate for any mounting node. So the only
178 // way this could possibly happen is if this was unmounted, if at all.
179 throw new Error('Unable to find node on an unmounted component.');
180 }
181
182 if (a.return !== b.return) {
183 // The return pointer of A and the return pointer of B point to different
184 // fibers. We assume that return pointers never criss-cross, so A must
185 // belong to the child set of A.return, and B must belong to the child
186 // set of B.return.
187 a = parentA;
188 b = parentB;
189 } else {
190 // The return pointers point to the same fiber. We'll have to use the
191 // default, slow path: scan the child sets of each parent alternate to see
192 // which child belongs to which set.
193 //
194 // Search parent A's child set
195 let didFindChild = false;
196 let child = parentA.child;
197 while (child) {
198 if (child === a) {
199 didFindChild = true;
200 a = parentA;
201 b = parentB;
202 break;
203 }
204 if (child === b) {
205 didFindChild = true;
206 b = parentA;
207 a = parentB;
208 break;
209 }
210 child = child.sibling;
211 }
212 if (!didFindChild) {
213 // Search parent B's child set
214 child = parentB.child;
215 while (child) {
216 if (child === a) {
217 didFindChild = true;
218 a = parentB;
219 b = parentA;
220 break;
221 }
222 if (child === b) {
223 didFindChild = true;
224 b = parentB;
225 a = parentA;
226 break;
227 }
228 child = child.sibling;
229 }
230
231 if (!didFindChild) {
232 throw new Error(
233 'Child was not found in either parent set. This indicates a bug ' +
234 'in React related to the return pointer. Please file an issue.',
235 );
236 }
237 }
238 }
239
240 if (a.alternate !== b) {
241 throw new Error(
242 "Return fibers should always be each others' alternates. " +
243 'This error is likely caused by a bug in React. Please file an issue.',
244 );
245 }
246 }
247
248 // If the root is not a host container, we're in a disconnected tree. I.e.
249 // unmounted.
250 if (a.tag !== HostRoot) {
251 throw new Error('Unable to find node on an unmounted component.');
252 }
253
254 if (a.stateNode.current === a) {
255 // We've determined that A is the current branch.
256 return fiber;
257 }
258 // Otherwise B has to be current branch.
259 return alternate;
260 }
261
262 export function findCurrentHostFiber(parent: Fiber): Fiber | null {
263 const currentParent = findCurrentFiberUsingSlowPath(parent);
264 return currentParent !== null
265 ? findCurrentHostFiberImpl(currentParent)
266 : null;
267 }
268
269 function findCurrentHostFiberImpl(node: Fiber): Fiber | null {
270 // Next we'll drill down this component to find the first HostComponent/Text.
271 const tag = node.tag;
272 if (
273 tag === HostComponent ||
274 tag === HostHoistable ||
275 tag === HostSingleton ||
276 tag === HostText
277 ) {
278 return node;
279 }
280
281 let child = node.child;
282 while (child !== null) {
283 const match = findCurrentHostFiberImpl(child);
284 if (match !== null) {
285 return match;
286 }
287 child = child.sibling;
288 }
289
290 return null;
291 }
292
293 export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
294 const currentParent = findCurrentFiberUsingSlowPath(parent);
295 return currentParent !== null
296 ? findCurrentHostFiberWithNoPortalsImpl(currentParent)
297 : null;
298 }
299
300 function findCurrentHostFiberWithNoPortalsImpl(node: Fiber): Fiber | null {
301 // Next we'll drill down this component to find the first HostComponent/Text.
302 const tag = node.tag;
303 if (
304 tag === HostComponent ||
305 tag === HostHoistable ||
306 tag === HostSingleton ||
307 tag === HostText
308 ) {
309 return node;
310 }
311
312 let child = node.child;
313 while (child !== null) {
314 if (child.tag !== HostPortal) {
315 const match = findCurrentHostFiberWithNoPortalsImpl(child);
316 if (match !== null) {
317 return match;
318 }
319 }
320 child = child.sibling;
321 }
322
323 return null;
324 }
325
326 export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
327 const memoizedState = fiber.memoizedState;
328 return (
329 fiber.tag === SuspenseComponent &&
330 memoizedState !== null &&
331 memoizedState.dehydrated === null
332 );
333 }
334
335 export function doesFiberContain(
336 parentFiber: Fiber,
337 childFiber: Fiber,
338 ): boolean {
339 let node: null | Fiber = childFiber;
340 const parentFiberAlternate = parentFiber.alternate;
341 while (node !== null) {
342 if (node === parentFiber || node === parentFiberAlternate) {
343 return true;
344 }
345 node = node.return;
346 }
347 return false;
348 }
349
350 export function traverseFragmentInstancesAndTextInstances<A, B, C>(
351 fragmentFiber: Fiber,
352 fn: (Fiber, A, B, C) => boolean,
353 a: A,
354 b: B,
355 c: C,
356 ): void {
357 traverseVisibleInstancesAndTextInstances(
358 fragmentFiber.child,
359 false,
360 fn,
361 a,
362 b,
363 c,
364 );
365 }
366
367 export function traverseFragmentInstancesAndTextInstancesDeeply<A, B, C>(
368 fragmentFiber: Fiber,
369 fn: (Fiber, A, B, C) => boolean,
370 a: A,
371 b: B,
372 c: C,
373 ): void {
374 traverseVisibleInstancesAndTextInstances(
375 fragmentFiber.child,
376 true,
377 fn,
378 a,
379 b,
380 c,
381 );
382 }
383
384 function traverseVisibleInstancesAndTextInstances<A, B, C>(
385 child: Fiber | null,
386 searchWithinHosts: boolean,
387 fn: (Fiber, A, B, C) => boolean,
388 a: A,
389 b: B,
390 c: C,
391 ): boolean {
392 while (child !== null) {
393 const isHostNode =
394 child.tag === HostComponent ||
395 child.tag === HostSingleton ||
396 (enableFragmentRefsTextNodes && child.tag === HostText);
397 if (isHostNode && fn(child, a, b, c)) {
398 return true;
399 } else if (
400 child.tag === OffscreenComponent &&
401 child.memoizedState !== null
402 ) {
403 // Skip hidden subtrees
404 } else {
405 if (
406 (searchWithinHosts ||
407 (child.tag !== HostComponent && child.tag !== HostSingleton)) &&
408 traverseVisibleInstancesAndTextInstances(
409 child.child,
410 searchWithinHosts,
411 fn,
412 a,
413 b,
414 c,
415 )
416 ) {
417 return true;
418 }
419 }
420 child = child.sibling;
421 }
422 return false;
423 }
424
425 export function getFragmentParentInstanceOrContainerFiber(
426 fiber: Fiber,
427 ): null | Fiber {
428 let parent = fiber.return;
429 while (parent !== null) {
430 if (
431 parent.tag === HostRoot ||
432 parent.tag === HostComponent ||
433 parent.tag === HostSingleton
434 ) {
435 return parent;
436 }
437 parent = parent.return;
438 }
439
440 return null;
441 }
442
443 export function fiberIsPortaledIntoHost(fiber: Fiber): boolean {
444 let foundPortalParent = false;
445 let parent = fiber.return;
446 while (parent !== null) {
447 if (parent.tag === HostPortal) {
448 foundPortalParent = true;
449 }
450 if (
451 parent.tag === HostRoot ||
452 parent.tag === HostComponent ||
453 parent.tag === HostSingleton
454 ) {
455 break;
456 }
457 parent = parent.return;
458 }
459 return foundPortalParent;
460 }
461
462 export function getFragmentPortalContainerInfo(fiber: Fiber): null | Container {
463 let parent = fiber.return;
464 while (parent !== null) {
465 if (parent.tag === HostPortal) {
466 return parent.stateNode.containerInfo as Container;
467 }
468 if (
469 parent.tag === HostRoot ||
470 parent.tag === HostComponent ||
471 parent.tag === HostSingleton
472 ) {
473 break;
474 }
475 parent = parent.return;
476 }
477 return null;
478 }
479
480 export function getFragmentInstanceOrTextInstanceSiblings(
481 fiber: Fiber,
482 ): [Fiber | null, Fiber | null] {
483 const result: [Fiber | null, Fiber | null] = [null, null];
484 const parentHostFiber = getFragmentParentInstanceOrContainerFiber(fiber);
485 if (parentHostFiber === null) {
486 return result;
487 }
488
489 findFragmentInstanceOrTextInstanceSiblings(
490 result,
491 fiber,
492 parentHostFiber.child,
493 {foundSelf: false},
494 );
495 return result;
496 }
497
498 /**
499 * Only collects HostText with enableFragmentRefsTextNodes enabled. Otherwise, only collects HostComponent.
500 * Returns true once the following host sibling has been found.
501 */
502 function findFragmentInstanceOrTextInstanceSiblings(
503 result: [Fiber | null, Fiber | null],
504 self: Fiber,
505 child: null | Fiber,
506 state: {foundSelf: boolean},
507 ): boolean {
508 while (child !== null) {
509 if (child === self) {
510 // Shared across recursive calls so ancestors can keep scanning for
511 // following host siblings after a nested empty fragment.
512 state.foundSelf = true;
513 child = child.sibling;
514 continue;
515 }
516 if (
517 child.tag === HostComponent ||
518 child.tag === HostSingleton ||
519 (enableFragmentRefsTextNodes && child.tag === HostText)
520 ) {
521 if (state.foundSelf) {
522 result[1] = child;
523 return true;
524 } else {
525 result[0] = child;
526 }
527 } else if (
528 child.tag === OffscreenComponent &&
529 child.memoizedState !== null
530 ) {
531 // Skip hidden subtrees
532 } else {
533 if (
534 findFragmentInstanceOrTextInstanceSiblings(
535 result,
536 self,
537 child.child,
538 state,
539 )
540 ) {
541 return true;
542 }
543 }
544 child = child.sibling;
545 }
546 return false;
547 }
548
549 export function getInstanceFromHostFiber<
550 I: Instance | TextInstance | Container,
551 >(fiber: Fiber): I {
552 switch (fiber.tag) {
553 case HostComponent:
554 case HostSingleton:
555 case HostText:
556 return fiber.stateNode;
557 case HostRoot:
558 return fiber.stateNode.containerInfo;
559 default:
560 throw new Error('Expected to find a host node. This is a bug in React.');
561 }
562 }
563
564 let searchTarget = null;
565 let searchBoundary = null;
566 function pushSearchTarget(target: null | Fiber): void {
567 searchTarget = target;
568 }
569 function popSearchTarget(): null | Fiber {
570 return searchTarget;
571 }
572 function pushSearchBoundary(value: null | Fiber): void {
573 searchBoundary = value;
574 }
575 function popSearchBoundary(): null | Fiber {
576 return searchBoundary;
577 }
578
579 export function getNextSiblingInstanceOrTextInstanceFiber(
580 fiber: Fiber,
581 ): null | Fiber {
582 traverseVisibleInstancesAndTextInstances(
583 fiber.sibling,
584 false,
585 findNextSibling,
586 );
587 const sibling = popSearchTarget();
588 pushSearchTarget(null);
589 return sibling;
590 }
591
592 function findNextSibling(child: Fiber): boolean {
593 pushSearchTarget(child);
594 return true;
595 }
596
597 export function isFiberContainedByFragment(
598 fiber: Fiber,
599 fragmentFiber: Fiber,
600 ): boolean {
601 let current: Fiber | null = fiber;
602 while (current !== null) {
603 if (
604 current.tag === Fragment &&
605 (current === fragmentFiber || current.alternate === fragmentFiber)
606 ) {
607 return true;
608 }
609 current = current.return;
610 }
611 return false;
612 }
613
614 export function isFragmentContainedByFiber(
615 fragmentFiber: Fiber,
616 otherFiber: Fiber,
617 ): boolean {
618 let current: Fiber | null = fragmentFiber;
619 const fiberHostParent: Fiber | null =
620 getFragmentParentInstanceOrContainerFiber(fragmentFiber);
621 while (current !== null) {
622 if (
623 (current.tag === HostComponent ||
624 current.tag === HostRoot ||
625 current.tag === HostSingleton) &&
626 (current === fiberHostParent || current.alternate === fiberHostParent)
627 ) {
628 return true;
629 }
630 current = current.return;
631 }
632 return false;
633 }
634
635 export function isFiberPreceding(fiber: Fiber, otherFiber: Fiber): boolean {
636 const commonAncestor = getLowestCommonAncestor(
637 fiber,
638 otherFiber,
639 getParentForFragmentAncestors,
640 );
641 if (commonAncestor === null) {
642 return false;
643 }
644 traverseVisibleInstancesAndTextInstances(
645 commonAncestor,
646 true,
647 isFiberPrecedingCheck,
648 otherFiber,
649 fiber,
650 );
651 const target = popSearchTarget();
652 pushSearchTarget(null);
653 return target !== null;
654 }
655
656 function isFiberPrecedingCheck(
657 child: Fiber,
658 target: Fiber,
659 boundary: Fiber,
660 ): boolean {
661 if (child === boundary) {
662 return true;
663 }
664 if (child === target) {
665 pushSearchTarget(child);
666 return true;
667 }
668 return false;
669 }
670
671 export function isFiberFollowing(fiber: Fiber, otherFiber: Fiber): boolean {
672 const commonAncestor = getLowestCommonAncestor(
673 fiber,
674 otherFiber,
675 getParentForFragmentAncestors,
676 );
677 if (commonAncestor === null) {
678 return false;
679 }
680 traverseVisibleInstancesAndTextInstances(
681 commonAncestor,
682 true,
683 isFiberFollowingCheck,
684 otherFiber,
685 fiber,
686 );
687 const target = popSearchTarget();
688 pushSearchTarget(null);
689 pushSearchBoundary(null);
690 return target !== null;
691 }
692
693 function isFiberFollowingCheck(
694 child: Fiber,
695 target: Fiber,
696 boundary: Fiber,
697 ): boolean {
698 if (child === boundary) {
699 pushSearchBoundary(child);
700 return false;
701 }
702 if (child === target) {
703 // The target is only following if we already found the boundary.
704 if (popSearchBoundary() !== null) {
705 pushSearchTarget(child);
706 }
707 return true;
708 }
709 return false;
710 }
711
712 function getParentForFragmentAncestors(inst: Fiber | null): Fiber | null {
713 if (inst === null) {
714 return null;
715 }
716 do {
717 inst = inst === null ? null : inst.return;
718 } while (
719 inst &&
720 inst.tag !== HostComponent &&
721 inst.tag !== HostSingleton &&
722 inst.tag !== HostRoot
723 );
724 if (inst) {
725 return inst;
726 }
727 return null;
728 }
729
730 /**
731 * Return the lowest common ancestor of A and B, or null if they are in
732 * different trees.
733 */
734 export function getLowestCommonAncestor(
735 instA: Fiber,
736 instB: Fiber,
737 getParent: (inst: Fiber | null) => Fiber | null,
738 ): Fiber | null {
739 let nodeA: null | Fiber = instA;
740 let nodeB: null | Fiber = instB;
741 let depthA = 0;
742 for (let tempA: null | Fiber = nodeA; tempA; tempA = getParent(tempA)) {
743 depthA++;
744 }
745 let depthB = 0;
746 for (let tempB: null | Fiber = nodeB; tempB; tempB = getParent(tempB)) {
747 depthB++;
748 }
749
750 // If A is deeper, crawl up.
751 while (depthA - depthB > 0) {
752 nodeA = getParent(nodeA);
753 depthA--;
754 }
755
756 // If B is deeper, crawl up.
757 while (depthB - depthA > 0) {
758 nodeB = getParent(nodeB);
759 depthB--;
760 }
761
762 // Walk in lockstep until we find a match.
763 let depth = depthA;
764 while (depth--) {
765 if (nodeA === nodeB || (nodeB !== null && nodeA === nodeB.alternate)) {
766 return nodeA;
767 }
768 nodeA = getParent(nodeA);
769 nodeB = getParent(nodeB);
770 }
771 return null;
772 }