1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910// UpdateQueue is a linked list of prioritized updates.11//12// Like fibers, update queues come in pairs: a current queue, which represents13// the visible state of the screen, and a work-in-progress queue, which can be14// mutated and processed asynchronously before it is committed — a form of15// 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 an19// update, we append it to the end of both queues. Each queue maintains a20// 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 than22// the current queue, since we always work on that one. The current queue's23// pointer is only updated during the commit phase, when we swap in the24// work-in-progress.25//26// For example:27//28// Current pointer: A - B - C - D - E - F29// Work-in-progress pointer: D - E - F30// ^31// The work-in-progress queue has32// processed more updates than current.33//34// The reason we append to both queues is because otherwise we might drop35// updates without ever processing them. For example, if we only add updates to36// the work-in-progress queue, some updates could be lost whenever a work-in37// -progress render restarts by cloning from current. Similarly, if we only add38// updates to the current queue, the updates will be lost whenever an already39// in-progress queue commits and swaps with the current queue. However, by40// adding to both queues, we guarantee that the update will be part of the next41// work-in-progress. (And because the work-in-progress queue becomes the42// current queue once it commits, there's no danger of applying the same43// update twice.)44//45// Prioritization46// --------------47//48// Updates are not sorted by priority, but by insertion; new updates are always49// appended to the end of the list.50//51// The priority is still important, though. When processing the update queue52// during the render phase, only the updates with sufficient priority are53// included in the result. If we skip an update because it has insufficient54// priority, it remains in the queue to be processed later, during a lower55// priority render. Crucially, all updates subsequent to a skipped update also56// remain in the queue *regardless of their priority*. That means high priority57// updates are sometimes processed twice, at two separate priorities. We also58// keep track of a base state, that represents the state before the first59// update in the queue is applied.60//61// For example:62//63// Given a base state of '', and the following queue of updates64//65// A1 - B2 - C1 - D266//67// where the number indicates the priority, and the update is applied to the68// previous state by appending a letter, React will process these updates as69// 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 B280// Result state: 'ABCD'81//82// Because we process updates in insertion order, and rebase high priority83// updates when preceding updates are skipped, the final result is deterministic84// regardless of priority. Intermediate state may vary according to system85// resources, but the final state is always the same.8687import type {Fiber, FiberRoot} from './ReactInternalTypes';88import type {Lanes, Lane} from './ReactFiberLane';8990import {91 NoLane,92 NoLanes,93 OffscreenLane,94 isSubsetOfLanes,95 mergeLanes,96 removeLanes,97 isTransitionLane,98 intersectLanes,99 markRootEntangled,100} from './ReactFiberLane';101import {102 enterDisallowedContextReadInDEV,103 exitDisallowedContextReadInDEV,104} from './ReactFiberNewContext';105import {106 Callback,107 Visibility,108 ShouldCapture,109 DidCapture,110} from './ReactFiberFlags';111import getComponentNameFromFiber from './getComponentNameFromFiber';112113import {StrictLegacyMode} from './ReactTypeOfMode';114import {115 markSkippedUpdateLanes,116 isUnsafeClassRenderPhaseUpdate,117 getWorkInProgressRootRenderLanes,118} from './ReactFiberWorkLoop';119import {120 enqueueConcurrentClassUpdate,121 unsafe_markUpdateLaneFromFiberToRoot,122} from './ReactFiberConcurrentUpdates';123import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook';124125import assign from 'shared/assign';126import {127 peekEntangledActionLane,128 peekEntangledActionThenable,129} from './ReactFiberAsyncAction';130131export type Update<State> = {132 lane: Lane,133134 tag: 0 | 1 | 2 | 3,135 payload: any,136 callback: (() => mixed) | null,137138 next: Update<State> | null,139};140141export type SharedQueue<State> = {142 pending: Update<State> | null,143 lanes: Lanes,144 hiddenCallbacks: Array<() => mixed> | null,145};146147export 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};154155export const UpdateState = 0;156export const ReplaceState = 1;157export const ForceUpdate = 2;158export const CaptureUpdate = 3;159160// Global state that is reset at the beginning of calling `processUpdateQueue`.161// It should only be read right after calling `processUpdateQueue`, via162// `checkHasForceUpdateAfterProcessing`.163let hasForceUpdate = false;164165let didWarnUpdateInsideUpdate;166let currentlyProcessingQueue: ?SharedQueue<$FlowFixMe>;167export let resetCurrentlyProcessingQueue: () => void;168if (__DEV__) {169 didWarnUpdateInsideUpdate = false;170 currentlyProcessingQueue = null;171 resetCurrentlyProcessingQueue = () => {172 currentlyProcessingQueue = null;173 };174}175176export 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}190191export 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}209210export function createUpdate(lane: Lane): Update<mixed> {211 const update: Update<mixed> = {212 lane,213214 tag: UpdateState,215 payload: null,216 callback: null,217218 next: null,219 };220 return update;221}222223export 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 }233234 const sharedQueue: SharedQueue<State> = (updateQueue as any).shared;235236 if (__DEV__) {237 if (238 currentlyProcessingQueue === sharedQueue &&239 !didWarnUpdateInsideUpdate240 ) {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 }252253 if (isUnsafeClassRenderPhaseUpdate(fiber)) {254 // This is an unsafe render phase update. Add directly to the update255 // 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;265266 // Update the childLanes even though we're most likely already rendering267 // this fiber. This is for backwards compatibility in the case where you268 // update a different component during render phase than the one that is269 // 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}275276export 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 }282283 const sharedQueue: SharedQueue<mixed> = (updateQueue as any).shared;284 if (isTransitionLane(lane)) {285 let queueLanes = sharedQueue.lanes;286287 // If any entangled lanes are no longer pending on the root, then they must288 // have finished. We can remove them from the shared queue, which represents289 // a superset of the actually pending lanes. In some cases we may entangle290 // 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);293294 // 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 if298 // the lane finished since the last time we entangled it. So we need to299 // entangle it again, just to be sure.300 markRootEntangled(root, newQueueLanes);301 }302}303304export function enqueueCapturedUpdate<State>(305 workInProgress: Fiber,306 capturedUpdate: Update<State>,307) {308 // Captured updates are updates that are thrown by a child during the render309 // 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;312313 // 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 when319 // we bail out on a parent fiber that then captures an error thrown by320 // a child. Since we want to append the update only to the work-in321 // -progress queue, we need to clone the updates. We usually clone during322 // processUpdateQueue, but that didn't happen in this case because we323 // 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,333334 tag: update.tag,335 payload: update.payload,336 // When this update is rebased, we should not fire its337 // callback again.338 callback: null,339340 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 null349 update = update.next;350 } while (update !== null);351352 // 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 }375376 // 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}385386function 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 function399 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 object417 return payload;418 }419 case CaptureUpdate: {420 workInProgress.flags =421 (workInProgress.flags & ~ShouldCapture) | DidCapture;422 }423 // Intentional fallthrough424 case UpdateState: {425 const payload = update.payload;426 let partialState;427 if (typeof payload === 'function') {428 // Updater function429 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 object446 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}462463let didReadFromEntangledAsyncAction: boolean = false;464465// Each call to processUpdateQueue should be accompanied by a call to this. It's466// only in a separate function because in updateHostRoot, it must happen after467// all the context stacks have been pushed to, to prevent a stack mismatch. A468// bit unfortunate.469export function suspendIfUpdateReadFromEntangledAsyncAction() {470 // Check if this update is part of a pending async action. If so, we'll471 // need to suspend until the action has finished, so that it's batched472 // together with future updates in the same action.473 // TODO: Once we support hooks inside useMemo (or an equivalent474 // memoization boundary like Forget), hoist this logic so that it only475 // 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 a480 // special object like `use` does so we can detect if it's captured481 // by userspace.482 throw entangledActionThenable;483 }484 }485}486487export function processUpdateQueue<State>(488 workInProgress: Fiber,489 props: any,490 instance: any,491 renderLanes: Lanes,492): void {493 didReadFromEntangledAsyncAction = false;494495 // This is always non-null on a ClassComponent or HostRoot496 const queue: UpdateQueue<State> = workInProgress.updateQueue as any;497498 hasForceUpdate = false;499500 if (__DEV__) {501 currentlyProcessingQueue = queue.shared;502 }503504 let firstBaseUpdate = queue.firstBaseUpdate;505 let lastBaseUpdate = queue.lastBaseUpdate;506507 // 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;511512 // The pending queue is circular. Disconnect the pointer between first513 // 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 queue518 if (lastBaseUpdate === null) {519 firstBaseUpdate = firstPendingUpdate;520 } else {521 lastBaseUpdate.next = firstPendingUpdate;522 }523 lastBaseUpdate = lastPendingUpdate;524525 // If there's a current queue, and it's different from the base queue, then526 // we need to transfer the updates to that queue, too. Because the base527 // queue is a singly-linked list with no cycles, we can append to both528 // lists and take advantage of structural sharing.529 // TODO: Pass `current` as argument530 const current = workInProgress.alternate;531 if (current !== null) {532 // This is always non-null on a ClassComponent or HostRoot533 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 }545546 // 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 renderLanes551 // from the original lanes.552 let newLanes: Lanes = NoLanes;553554 let newBaseState = null;555 let newFirstBaseUpdate = null;556 let newLastBaseUpdate: null | Update<State> = null;557558 let update: Update<State> = firstBaseUpdate;559 do {560 // An extra OffscreenLane bit is added to updates that were made to561 // a hidden tree, so that we can distinguish them from updates that were562 // already there when the tree was hidden.563 const updateLane = removeLanes(update.lane, OffscreenLane);564 const isHiddenUpdate = updateLane !== update.lane;565566 // Check if this update was made while the tree was hidden. If so, then567 // it's not a "base" update and we should disregard the extra base lanes568 // that were added to renderLanes when we entered the Offscreen tree.569 const shouldSkipUpdate = isHiddenUpdate570 ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)571 : !isSubsetOfLanes(renderLanes, updateLane);572573 if (shouldSkipUpdate) {574 // Priority is insufficient. Skip this update. If this is the first575 // skipped update, the previous update/state is the new base576 // update/state.577 const clone: Update<State> = {578 lane: updateLane,579580 tag: update.tag,581 payload: update.payload,582 callback: update.callback,583584 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.596597 // 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's599 // batched together with future updates in the same action.600 if (updateLane !== NoLane && updateLane === peekEntangledActionLane()) {601 didReadFromEntangledAsyncAction = true;602 }603604 if (newLastBaseUpdate !== null) {605 const clone: Update<State> = {606 // This update is going to be committed so we never want uncommit607 // it. Using NoLane works because 0 is a subset of all bitmasks, so608 // this will never be skipped by the check above.609 lane: NoLane,610611 tag: update.tag,612 payload: update.payload,613614 // When this update is rebased, we should not fire its615 // callback again.616 callback: null,617618 next: null,619 };620 newLastBaseUpdate = newLastBaseUpdate.next = clone;621 }622623 // 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 null647 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 new654 // 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 we657 // 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);667668 if (newLastBaseUpdate === null) {669 newBaseState = newState;670 }671672 queue.baseState = newBaseState as any as State;673 queue.firstBaseUpdate = newFirstBaseUpdate;674 queue.lastBaseUpdate = newLastBaseUpdate;675676 // $FlowFixMe[invalid-compare]677 if (firstBaseUpdate === null) {678 // `queue.lanes` is used for entangling transitions. We can set it back to679 // zero once the queue is empty.680 queue.shared.lanes = NoLanes;681 }682683 // 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 to685 // expiration time are props and context. We're already in the middle of the686 // begin phase by the time we start processing the queue, so we've already687 // dealt with the props. Context in components that specify688 // shouldComponentUpdate is tricky; but we'll have to account for689 // that regardless.690 markSkippedUpdateLanes(newLanes);691 workInProgress.lanes = newLanes;692 workInProgress.memoizedState = newState;693 }694695 if (__DEV__) {696 currentlyProcessingQueue = null;697 }698}699700function 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 }707708 callback.call(context);709}710711export function resetHasForceUpdateBeforeProcessing() {712 hasForceUpdate = false;713}714715export function checkHasForceUpdateAfterProcessing(): boolean {716 return hasForceUpdate;717}718719export function deferHiddenCallbacks<State>(720 updateQueue: UpdateQueue<State>,721): void {722 // When an update finishes on a hidden component, its callback should not723 // be fired until/unless the component is made visible again. Stash the724 // 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}736737export function commitHiddenCallbacks<State>(738 updateQueue: UpdateQueue<State>,739 context: any,740): void {741 // This component is switching from hidden -> visible. Commit any callbacks742 // 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}752753export 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}
Findings
✓ No findings reported for this file.