main
js 765 lines 25.9 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 // UpdateQueue is a linked list of prioritized updates.
11 //
12 // Like fibers, update queues come in pairs: a current queue, which represents
13 // the visible state of the screen, and a work-in-progress queue, which can be
14 // mutated and processed asynchronously before it is committed — a form of
15 // double buffering. If a work-in-progress render is discarded before finishing,
16 // we create a new work-in-progress by cloning the current queue.
17 //
18 // Both queues share a persistent, singly-linked list structure. To schedule an
19 // update, we append it to the end of both queues. Each queue maintains a
20 // pointer to first update in the persistent list that hasn't been processed.
21 // The work-in-progress pointer always has a position equal to or greater than
22 // the current queue, since we always work on that one. The current queue's
23 // pointer is only updated during the commit phase, when we swap in the
24 // work-in-progress.
25 //
26 // For example:
27 //
28 // Current pointer: A - B - C - D - E - F
29 // Work-in-progress pointer: D - E - F
30 // ^
31 // The work-in-progress queue has
32 // processed more updates than current.
33 //
34 // The reason we append to both queues is because otherwise we might drop
35 // updates without ever processing them. For example, if we only add updates to
36 // the work-in-progress queue, some updates could be lost whenever a work-in
37 // -progress render restarts by cloning from current. Similarly, if we only add
38 // updates to the current queue, the updates will be lost whenever an already
39 // in-progress queue commits and swaps with the current queue. However, by
40 // adding to both queues, we guarantee that the update will be part of the next
41 // work-in-progress. (And because the work-in-progress queue becomes the
42 // current queue once it commits, there's no danger of applying the same
43 // update twice.)
44 //
45 // Prioritization
46 // --------------
47 //
48 // Updates are not sorted by priority, but by insertion; new updates are always
49 // appended to the end of the list.
50 //
51 // The priority is still important, though. When processing the update queue
52 // during the render phase, only the updates with sufficient priority are
53 // included in the result. If we skip an update because it has insufficient
54 // priority, it remains in the queue to be processed later, during a lower
55 // priority render. Crucially, all updates subsequent to a skipped update also
56 // remain in the queue *regardless of their priority*. That means high priority
57 // updates are sometimes processed twice, at two separate priorities. We also
58 // keep track of a base state, that represents the state before the first
59 // update in the queue is applied.
60 //
61 // For example:
62 //
63 // Given a base state of '', and the following queue of updates
64 //
65 // A1 - B2 - C1 - D2
66 //
67 // where the number indicates the priority, and the update is applied to the
68 // previous state by appending a letter, React will process these updates as
69 // two separate renders, one per distinct priority level:
70 //
71 // First render, at priority 1:
72 // Base state: ''
73 // Updates: [A1, C1]
74 // Result state: 'AC'
75 //
76 // Second render, at priority 2:
77 // Base state: 'A' <- The base state does not include C1,
78 // because B2 was skipped.
79 // Updates: [B2, C1, D2] <- C1 was rebased on top of B2
80 // Result state: 'ABCD'
81 //
82 // Because we process updates in insertion order, and rebase high priority
83 // updates when preceding updates are skipped, the final result is deterministic
84 // regardless of priority. Intermediate state may vary according to system
85 // resources, but the final state is always the same.
86
87 import type {Fiber, FiberRoot} from './ReactInternalTypes';
88 import type {Lanes, Lane} from './ReactFiberLane';
89
90 import {
91 NoLane,
92 NoLanes,
93 OffscreenLane,
94 isSubsetOfLanes,
95 mergeLanes,
96 removeLanes,
97 isTransitionLane,
98 intersectLanes,
99 markRootEntangled,
100 } from './ReactFiberLane';
101 import {
102 enterDisallowedContextReadInDEV,
103 exitDisallowedContextReadInDEV,
104 } from './ReactFiberNewContext';
105 import {
106 Callback,
107 Visibility,
108 ShouldCapture,
109 DidCapture,
110 } from './ReactFiberFlags';
111 import getComponentNameFromFiber from './getComponentNameFromFiber';
112
113 import {StrictLegacyMode} from './ReactTypeOfMode';
114 import {
115 markSkippedUpdateLanes,
116 isUnsafeClassRenderPhaseUpdate,
117 getWorkInProgressRootRenderLanes,
118 } from './ReactFiberWorkLoop';
119 import {
120 enqueueConcurrentClassUpdate,
121 unsafe_markUpdateLaneFromFiberToRoot,
122 } from './ReactFiberConcurrentUpdates';
123 import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook';
124
125 import assign from 'shared/assign';
126 import {
127 peekEntangledActionLane,
128 peekEntangledActionThenable,
129 } from './ReactFiberAsyncAction';
130
131 export type Update<State> = {
132 lane: Lane,
133
134 tag: 0 | 1 | 2 | 3,
135 payload: any,
136 callback: (() => mixed) | null,
137
138 next: Update<State> | null,
139 };
140
141 export type SharedQueue<State> = {
142 pending: Update<State> | null,
143 lanes: Lanes,
144 hiddenCallbacks: Array<() => mixed> | null,
145 };
146
147 export type UpdateQueue<State> = {
148 baseState: State,
149 firstBaseUpdate: Update<State> | null,
150 lastBaseUpdate: Update<State> | null,
151 shared: SharedQueue<State>,
152 callbacks: Array<() => mixed> | null,
153 };
154
155 export const UpdateState = 0;
156 export const ReplaceState = 1;
157 export const ForceUpdate = 2;
158 export const CaptureUpdate = 3;
159
160 // Global state that is reset at the beginning of calling `processUpdateQueue`.
161 // It should only be read right after calling `processUpdateQueue`, via
162 // `checkHasForceUpdateAfterProcessing`.
163 let hasForceUpdate = false;
164
165 let didWarnUpdateInsideUpdate;
166 let currentlyProcessingQueue: ?SharedQueue<$FlowFixMe>;
167 export let resetCurrentlyProcessingQueue: () => void;
168 if (__DEV__) {
169 didWarnUpdateInsideUpdate = false;
170 currentlyProcessingQueue = null;
171 resetCurrentlyProcessingQueue = () => {
172 currentlyProcessingQueue = null;
173 };
174 }
175
176 export function initializeUpdateQueue<State>(fiber: Fiber): void {
177 const queue: UpdateQueue<State> = {
178 baseState: fiber.memoizedState,
179 firstBaseUpdate: null,
180 lastBaseUpdate: null,
181 shared: {
182 pending: null,
183 lanes: NoLanes,
184 hiddenCallbacks: null,
185 },
186 callbacks: null,
187 };
188 fiber.updateQueue = queue;
189 }
190
191 export function cloneUpdateQueue<State>(
192 current: Fiber,
193 workInProgress: Fiber,
194 ): void {
195 // Clone the update queue from current. Unless it's already a clone.
196 const queue: UpdateQueue<State> = workInProgress.updateQueue as any;
197 const currentQueue: UpdateQueue<State> = current.updateQueue as any;
198 if (queue === currentQueue) {
199 const clone: UpdateQueue<State> = {
200 baseState: currentQueue.baseState,
201 firstBaseUpdate: currentQueue.firstBaseUpdate,
202 lastBaseUpdate: currentQueue.lastBaseUpdate,
203 shared: currentQueue.shared,
204 callbacks: null,
205 };
206 workInProgress.updateQueue = clone;
207 }
208 }
209
210 export function createUpdate(lane: Lane): Update<mixed> {
211 const update: Update<mixed> = {
212 lane,
213
214 tag: UpdateState,
215 payload: null,
216 callback: null,
217
218 next: null,
219 };
220 return update;
221 }
222
223 export function enqueueUpdate<State>(
224 fiber: Fiber,
225 update: Update<State>,
226 lane: Lane,
227 ): FiberRoot | null {
228 const updateQueue = fiber.updateQueue;
229 if (updateQueue === null) {
230 // Only occurs if the fiber has been unmounted.
231 return null;
232 }
233
234 const sharedQueue: SharedQueue<State> = (updateQueue as any).shared;
235
236 if (__DEV__) {
237 if (
238 currentlyProcessingQueue === sharedQueue &&
239 !didWarnUpdateInsideUpdate
240 ) {
241 const componentName = getComponentNameFromFiber(fiber);
242 console.error(
243 'An update (setState, replaceState, or forceUpdate) was scheduled ' +
244 'from inside an update function. Update functions should be pure, ' +
245 'with zero side-effects. Consider using componentDidUpdate or a ' +
246 'callback.\n\nPlease update the following component: %s',
247 componentName,
248 );
249 didWarnUpdateInsideUpdate = true;
250 }
251 }
252
253 if (isUnsafeClassRenderPhaseUpdate(fiber)) {
254 // This is an unsafe render phase update. Add directly to the update
255 // queue so we can process it immediately during the current render.
256 const pending = sharedQueue.pending;
257 if (pending === null) {
258 // This is the first update. Create a circular list.
259 update.next = update;
260 } else {
261 update.next = pending.next;
262 pending.next = update;
263 }
264 sharedQueue.pending = update;
265
266 // Update the childLanes even though we're most likely already rendering
267 // this fiber. This is for backwards compatibility in the case where you
268 // update a different component during render phase than the one that is
269 // currently renderings (a pattern that is accompanied by a warning).
270 return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
271 } else {
272 return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
273 }
274 }
275
276 export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
277 const updateQueue = fiber.updateQueue;
278 if (updateQueue === null) {
279 // Only occurs if the fiber has been unmounted.
280 return;
281 }
282
283 const sharedQueue: SharedQueue<mixed> = (updateQueue as any).shared;
284 if (isTransitionLane(lane)) {
285 let queueLanes = sharedQueue.lanes;
286
287 // If any entangled lanes are no longer pending on the root, then they must
288 // have finished. We can remove them from the shared queue, which represents
289 // a superset of the actually pending lanes. In some cases we may entangle
290 // more than we need to, but that's OK. In fact it's worse if we *don't*
291 // entangle when we should.
292 queueLanes = intersectLanes(queueLanes, root.pendingLanes);
293
294 // Entangle the new transition lane with the other transition lanes.
295 const newQueueLanes = mergeLanes(queueLanes, lane);
296 sharedQueue.lanes = newQueueLanes;
297 // Even if queue.lanes already include lane, we don't know for certain if
298 // the lane finished since the last time we entangled it. So we need to
299 // entangle it again, just to be sure.
300 markRootEntangled(root, newQueueLanes);
301 }
302 }
303
304 export function enqueueCapturedUpdate<State>(
305 workInProgress: Fiber,
306 capturedUpdate: Update<State>,
307 ) {
308 // Captured updates are updates that are thrown by a child during the render
309 // phase. They should be discarded if the render is aborted. Therefore,
310 // we should only put them on the work-in-progress queue, not the current one.
311 let queue: UpdateQueue<State> = workInProgress.updateQueue as any;
312
313 // Check if the work-in-progress queue is a clone.
314 const current = workInProgress.alternate;
315 if (current !== null) {
316 const currentQueue: UpdateQueue<State> = current.updateQueue as any;
317 if (queue === currentQueue) {
318 // The work-in-progress queue is the same as current. This happens when
319 // we bail out on a parent fiber that then captures an error thrown by
320 // a child. Since we want to append the update only to the work-in
321 // -progress queue, we need to clone the updates. We usually clone during
322 // processUpdateQueue, but that didn't happen in this case because we
323 // skipped over the parent when we bailed out.
324 let newFirst = null;
325 let newLast = null;
326 const firstBaseUpdate = queue.firstBaseUpdate;
327 if (firstBaseUpdate !== null) {
328 // Loop through the updates and clone them.
329 let update: Update<State> = firstBaseUpdate;
330 do {
331 const clone: Update<State> = {
332 lane: update.lane,
333
334 tag: update.tag,
335 payload: update.payload,
336 // When this update is rebased, we should not fire its
337 // callback again.
338 callback: null,
339
340 next: null,
341 };
342 if (newLast === null) {
343 newFirst = newLast = clone;
344 } else {
345 newLast.next = clone;
346 newLast = clone;
347 }
348 // $FlowFixMe[incompatible-type] we bail out when we get a null
349 update = update.next;
350 } while (update !== null);
351
352 // Append the captured update the end of the cloned list.
353 // $FlowFixMe[invalid-compare]
354 if (newLast === null) {
355 newFirst = newLast = capturedUpdate;
356 } else {
357 newLast.next = capturedUpdate;
358 newLast = capturedUpdate;
359 }
360 } else {
361 // There are no base updates.
362 newFirst = newLast = capturedUpdate;
363 }
364 queue = {
365 baseState: currentQueue.baseState,
366 firstBaseUpdate: newFirst,
367 lastBaseUpdate: newLast,
368 shared: currentQueue.shared,
369 callbacks: currentQueue.callbacks,
370 };
371 workInProgress.updateQueue = queue;
372 return;
373 }
374 }
375
376 // Append the update to the end of the list.
377 const lastBaseUpdate = queue.lastBaseUpdate;
378 if (lastBaseUpdate === null) {
379 queue.firstBaseUpdate = capturedUpdate;
380 } else {
381 lastBaseUpdate.next = capturedUpdate;
382 }
383 queue.lastBaseUpdate = capturedUpdate;
384 }
385
386 function getStateFromUpdate<State>(
387 workInProgress: Fiber,
388 queue: UpdateQueue<State>,
389 update: Update<State>,
390 prevState: State,
391 nextProps: any,
392 instance: any,
393 ): any {
394 switch (update.tag) {
395 case ReplaceState: {
396 const payload = update.payload;
397 if (typeof payload === 'function') {
398 // Updater function
399 if (__DEV__) {
400 enterDisallowedContextReadInDEV();
401 }
402 const nextState = payload.call(instance, prevState, nextProps);
403 if (__DEV__) {
404 if (workInProgress.mode & StrictLegacyMode) {
405 setIsStrictModeForDevtools(true);
406 try {
407 payload.call(instance, prevState, nextProps);
408 } finally {
409 setIsStrictModeForDevtools(false);
410 }
411 }
412 exitDisallowedContextReadInDEV();
413 }
414 return nextState;
415 }
416 // State object
417 return payload;
418 }
419 case CaptureUpdate: {
420 workInProgress.flags =
421 (workInProgress.flags & ~ShouldCapture) | DidCapture;
422 }
423 // Intentional fallthrough
424 case UpdateState: {
425 const payload = update.payload;
426 let partialState;
427 if (typeof payload === 'function') {
428 // Updater function
429 if (__DEV__) {
430 enterDisallowedContextReadInDEV();
431 }
432 partialState = payload.call(instance, prevState, nextProps);
433 if (__DEV__) {
434 if (workInProgress.mode & StrictLegacyMode) {
435 setIsStrictModeForDevtools(true);
436 try {
437 payload.call(instance, prevState, nextProps);
438 } finally {
439 setIsStrictModeForDevtools(false);
440 }
441 }
442 exitDisallowedContextReadInDEV();
443 }
444 } else {
445 // Partial state object
446 partialState = payload;
447 }
448 if (partialState === null || partialState === undefined) {
449 // Null and undefined are treated as no-ops.
450 return prevState;
451 }
452 // Merge the partial state and the previous state.
453 return assign({}, prevState, partialState);
454 }
455 case ForceUpdate: {
456 hasForceUpdate = true;
457 return prevState;
458 }
459 }
460 return prevState;
461 }
462
463 let didReadFromEntangledAsyncAction: boolean = false;
464
465 // Each call to processUpdateQueue should be accompanied by a call to this. It's
466 // only in a separate function because in updateHostRoot, it must happen after
467 // all the context stacks have been pushed to, to prevent a stack mismatch. A
468 // bit unfortunate.
469 export function suspendIfUpdateReadFromEntangledAsyncAction() {
470 // Check if this update is part of a pending async action. If so, we'll
471 // need to suspend until the action has finished, so that it's batched
472 // together with future updates in the same action.
473 // TODO: Once we support hooks inside useMemo (or an equivalent
474 // memoization boundary like Forget), hoist this logic so that it only
475 // suspends if the memo boundary produces a new value.
476 if (didReadFromEntangledAsyncAction) {
477 const entangledActionThenable = peekEntangledActionThenable();
478 if (entangledActionThenable !== null) {
479 // TODO: Instead of the throwing the thenable directly, throw a
480 // special object like `use` does so we can detect if it's captured
481 // by userspace.
482 throw entangledActionThenable;
483 }
484 }
485 }
486
487 export function processUpdateQueue<State>(
488 workInProgress: Fiber,
489 props: any,
490 instance: any,
491 renderLanes: Lanes,
492 ): void {
493 didReadFromEntangledAsyncAction = false;
494
495 // This is always non-null on a ClassComponent or HostRoot
496 const queue: UpdateQueue<State> = workInProgress.updateQueue as any;
497
498 hasForceUpdate = false;
499
500 if (__DEV__) {
501 currentlyProcessingQueue = queue.shared;
502 }
503
504 let firstBaseUpdate = queue.firstBaseUpdate;
505 let lastBaseUpdate = queue.lastBaseUpdate;
506
507 // Check if there are pending updates. If so, transfer them to the base queue.
508 let pendingQueue = queue.shared.pending;
509 if (pendingQueue !== null) {
510 queue.shared.pending = null;
511
512 // The pending queue is circular. Disconnect the pointer between first
513 // and last so that it's non-circular.
514 const lastPendingUpdate = pendingQueue;
515 const firstPendingUpdate = lastPendingUpdate.next;
516 lastPendingUpdate.next = null;
517 // Append pending updates to base queue
518 if (lastBaseUpdate === null) {
519 firstBaseUpdate = firstPendingUpdate;
520 } else {
521 lastBaseUpdate.next = firstPendingUpdate;
522 }
523 lastBaseUpdate = lastPendingUpdate;
524
525 // If there's a current queue, and it's different from the base queue, then
526 // we need to transfer the updates to that queue, too. Because the base
527 // queue is a singly-linked list with no cycles, we can append to both
528 // lists and take advantage of structural sharing.
529 // TODO: Pass `current` as argument
530 const current = workInProgress.alternate;
531 if (current !== null) {
532 // This is always non-null on a ClassComponent or HostRoot
533 const currentQueue: UpdateQueue<State> = current.updateQueue as any;
534 const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
535 if (currentLastBaseUpdate !== lastBaseUpdate) {
536 if (currentLastBaseUpdate === null) {
537 currentQueue.firstBaseUpdate = firstPendingUpdate;
538 } else {
539 currentLastBaseUpdate.next = firstPendingUpdate;
540 }
541 currentQueue.lastBaseUpdate = lastPendingUpdate;
542 }
543 }
544 }
545
546 // These values may change as we process the queue.
547 if (firstBaseUpdate !== null) {
548 // Iterate through the list of updates to compute the result.
549 let newState = queue.baseState;
550 // TODO: Don't need to accumulate this. Instead, we can remove renderLanes
551 // from the original lanes.
552 let newLanes: Lanes = NoLanes;
553
554 let newBaseState = null;
555 let newFirstBaseUpdate = null;
556 let newLastBaseUpdate: null | Update<State> = null;
557
558 let update: Update<State> = firstBaseUpdate;
559 do {
560 // An extra OffscreenLane bit is added to updates that were made to
561 // a hidden tree, so that we can distinguish them from updates that were
562 // already there when the tree was hidden.
563 const updateLane = removeLanes(update.lane, OffscreenLane);
564 const isHiddenUpdate = updateLane !== update.lane;
565
566 // Check if this update was made while the tree was hidden. If so, then
567 // it's not a "base" update and we should disregard the extra base lanes
568 // that were added to renderLanes when we entered the Offscreen tree.
569 const shouldSkipUpdate = isHiddenUpdate
570 ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
571 : !isSubsetOfLanes(renderLanes, updateLane);
572
573 if (shouldSkipUpdate) {
574 // Priority is insufficient. Skip this update. If this is the first
575 // skipped update, the previous update/state is the new base
576 // update/state.
577 const clone: Update<State> = {
578 lane: updateLane,
579
580 tag: update.tag,
581 payload: update.payload,
582 callback: update.callback,
583
584 next: null,
585 };
586 if (newLastBaseUpdate === null) {
587 newFirstBaseUpdate = newLastBaseUpdate = clone;
588 newBaseState = newState;
589 } else {
590 newLastBaseUpdate = newLastBaseUpdate.next = clone;
591 }
592 // Update the remaining priority in the queue.
593 newLanes = mergeLanes(newLanes, updateLane);
594 } else {
595 // This update does have sufficient priority.
596
597 // Check if this update is part of a pending async action. If so,
598 // we'll need to suspend until the action has finished, so that it's
599 // batched together with future updates in the same action.
600 if (updateLane !== NoLane && updateLane === peekEntangledActionLane()) {
601 didReadFromEntangledAsyncAction = true;
602 }
603
604 if (newLastBaseUpdate !== null) {
605 const clone: Update<State> = {
606 // This update is going to be committed so we never want uncommit
607 // it. Using NoLane works because 0 is a subset of all bitmasks, so
608 // this will never be skipped by the check above.
609 lane: NoLane,
610
611 tag: update.tag,
612 payload: update.payload,
613
614 // When this update is rebased, we should not fire its
615 // callback again.
616 callback: null,
617
618 next: null,
619 };
620 newLastBaseUpdate = newLastBaseUpdate.next = clone;
621 }
622
623 // Process this update.
624 newState = getStateFromUpdate(
625 workInProgress,
626 queue,
627 update,
628 newState,
629 props,
630 instance,
631 );
632 const callback = update.callback;
633 if (callback !== null) {
634 workInProgress.flags |= Callback;
635 if (isHiddenUpdate) {
636 workInProgress.flags |= Visibility;
637 }
638 const callbacks = queue.callbacks;
639 if (callbacks === null) {
640 queue.callbacks = [callback];
641 } else {
642 callbacks.push(callback);
643 }
644 }
645 }
646 // $FlowFixMe[incompatible-type] we bail out when we get a null
647 update = update.next;
648 if (update === null) {
649 pendingQueue = queue.shared.pending;
650 if (pendingQueue === null) {
651 break;
652 } else {
653 // An update was scheduled from inside a reducer. Add the new
654 // pending updates to the end of the list and keep processing.
655 const lastPendingUpdate = pendingQueue;
656 // Intentionally unsound. Pending updates form a circular list, but we
657 // unravel them when transferring them to the base queue.
658 const firstPendingUpdate =
659 lastPendingUpdate.next as any as Update<State>;
660 lastPendingUpdate.next = null;
661 update = firstPendingUpdate;
662 queue.lastBaseUpdate = lastPendingUpdate;
663 queue.shared.pending = null;
664 }
665 }
666 } while (true);
667
668 if (newLastBaseUpdate === null) {
669 newBaseState = newState;
670 }
671
672 queue.baseState = newBaseState as any as State;
673 queue.firstBaseUpdate = newFirstBaseUpdate;
674 queue.lastBaseUpdate = newLastBaseUpdate;
675
676 // $FlowFixMe[invalid-compare]
677 if (firstBaseUpdate === null) {
678 // `queue.lanes` is used for entangling transitions. We can set it back to
679 // zero once the queue is empty.
680 queue.shared.lanes = NoLanes;
681 }
682
683 // Set the remaining expiration time to be whatever is remaining in the queue.
684 // This should be fine because the only two other things that contribute to
685 // expiration time are props and context. We're already in the middle of the
686 // begin phase by the time we start processing the queue, so we've already
687 // dealt with the props. Context in components that specify
688 // shouldComponentUpdate is tricky; but we'll have to account for
689 // that regardless.
690 markSkippedUpdateLanes(newLanes);
691 workInProgress.lanes = newLanes;
692 workInProgress.memoizedState = newState;
693 }
694
695 if (__DEV__) {
696 currentlyProcessingQueue = null;
697 }
698 }
699
700 function callCallback(callback: () => mixed, context: any) {
701 if (typeof callback !== 'function') {
702 throw new Error(
703 'Invalid argument passed as callback. Expected a function. Instead ' +
704 `received: ${callback}`,
705 );
706 }
707
708 callback.call(context);
709 }
710
711 export function resetHasForceUpdateBeforeProcessing() {
712 hasForceUpdate = false;
713 }
714
715 export function checkHasForceUpdateAfterProcessing(): boolean {
716 return hasForceUpdate;
717 }
718
719 export function deferHiddenCallbacks<State>(
720 updateQueue: UpdateQueue<State>,
721 ): void {
722 // When an update finishes on a hidden component, its callback should not
723 // be fired until/unless the component is made visible again. Stash the
724 // callback on the shared queue object so it can be fired later.
725 const newHiddenCallbacks = updateQueue.callbacks;
726 if (newHiddenCallbacks !== null) {
727 const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;
728 if (existingHiddenCallbacks === null) {
729 updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;
730 } else {
731 updateQueue.shared.hiddenCallbacks =
732 existingHiddenCallbacks.concat(newHiddenCallbacks);
733 }
734 }
735 }
736
737 export function commitHiddenCallbacks<State>(
738 updateQueue: UpdateQueue<State>,
739 context: any,
740 ): void {
741 // This component is switching from hidden -> visible. Commit any callbacks
742 // that were previously deferred.
743 const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;
744 if (hiddenCallbacks !== null) {
745 updateQueue.shared.hiddenCallbacks = null;
746 for (let i = 0; i < hiddenCallbacks.length; i++) {
747 const callback = hiddenCallbacks[i];
748 callCallback(callback, context);
749 }
750 }
751 }
752
753 export function commitCallbacks<State>(
754 updateQueue: UpdateQueue<State>,
755 context: any,
756 ): void {
757 const callbacks = updateQueue.callbacks;
758 if (callbacks !== null) {
759 updateQueue.callbacks = null;
760 for (let i = 0; i < callbacks.length; i++) {
761 const callback = callbacks[i];
762 callCallback(callback, context);
763 }
764 }
765 }