/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ // UpdateQueue is a linked list of prioritized updates. // // Like fibers, update queues come in pairs: a current queue, which represents // the visible state of the screen, and a work-in-progress queue, which can be // mutated and processed asynchronously before it is committed — a form of // double buffering. If a work-in-progress render is discarded before finishing, // we create a new work-in-progress by cloning the current queue. // // Both queues share a persistent, singly-linked list structure. To schedule an // update, we append it to the end of both queues. Each queue maintains a // pointer to first update in the persistent list that hasn't been processed. // The work-in-progress pointer always has a position equal to or greater than // the current queue, since we always work on that one. The current queue's // pointer is only updated during the commit phase, when we swap in the // work-in-progress. // // For example: // // Current pointer: A - B - C - D - E - F // Work-in-progress pointer: D - E - F // ^ // The work-in-progress queue has // processed more updates than current. // // The reason we append to both queues is because otherwise we might drop // updates without ever processing them. For example, if we only add updates to // the work-in-progress queue, some updates could be lost whenever a work-in // -progress render restarts by cloning from current. Similarly, if we only add // updates to the current queue, the updates will be lost whenever an already // in-progress queue commits and swaps with the current queue. However, by // adding to both queues, we guarantee that the update will be part of the next // work-in-progress. (And because the work-in-progress queue becomes the // current queue once it commits, there's no danger of applying the same // update twice.) // // Prioritization // -------------- // // Updates are not sorted by priority, but by insertion; new updates are always // appended to the end of the list. // // The priority is still important, though. When processing the update queue // during the render phase, only the updates with sufficient priority are // included in the result. If we skip an update because it has insufficient // priority, it remains in the queue to be processed later, during a lower // priority render. Crucially, all updates subsequent to a skipped update also // remain in the queue *regardless of their priority*. That means high priority // updates are sometimes processed twice, at two separate priorities. We also // keep track of a base state, that represents the state before the first // update in the queue is applied. // // For example: // // Given a base state of '', and the following queue of updates // // A1 - B2 - C1 - D2 // // where the number indicates the priority, and the update is applied to the // previous state by appending a letter, React will process these updates as // two separate renders, one per distinct priority level: // // First render, at priority 1: // Base state: '' // Updates: [A1, C1] // Result state: 'AC' // // Second render, at priority 2: // Base state: 'A' <- The base state does not include C1, // because B2 was skipped. // Updates: [B2, C1, D2] <- C1 was rebased on top of B2 // Result state: 'ABCD' // // Because we process updates in insertion order, and rebase high priority // updates when preceding updates are skipped, the final result is deterministic // regardless of priority. Intermediate state may vary according to system // resources, but the final state is always the same. import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; import {NoWork} from './ReactFiberExpirationTime'; import { enterDisallowedContextReadInDEV, exitDisallowedContextReadInDEV, } from './ReactFiberNewContext'; import {Callback, ShouldCapture, DidCapture} from 'shared/ReactSideEffectTags'; import {ClassComponent} from 'shared/ReactWorkTags'; import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, } from 'shared/ReactFeatureFlags'; import {StrictMode} from './ReactTypeOfMode'; import { markRenderEventTimeAndConfig, markUnprocessedUpdateTime, } from './ReactFiberWorkLoop'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration'; export type Update = { expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, tag: 0 | 1 | 2 | 3, payload: any, callback: (() => mixed) | null, next: Update | null, nextEffect: Update | null, //DEV only priority?: ReactPriorityLevel, }; export type UpdateQueue = { baseState: State, firstUpdate: Update | null, lastUpdate: Update | null, firstCapturedUpdate: Update | null, lastCapturedUpdate: Update | null, firstEffect: Update | null, lastEffect: Update | null, firstCapturedEffect: Update | null, lastCapturedEffect: Update | null, }; export const UpdateState = 0; export const ReplaceState = 1; export const ForceUpdate = 2; export const CaptureUpdate = 3; // Global state that is reset at the beginning of calling `processUpdateQueue`. // It should only be read right after calling `processUpdateQueue`, via // `checkHasForceUpdateAfterProcessing`. let hasForceUpdate = false; let didWarnUpdateInsideUpdate; let currentlyProcessingQueue; export let resetCurrentlyProcessingQueue; if (__DEV__) { didWarnUpdateInsideUpdate = false; currentlyProcessingQueue = null; resetCurrentlyProcessingQueue = () => { currentlyProcessingQueue = null; }; } export function createUpdateQueue(baseState: State): UpdateQueue { const queue: UpdateQueue = { baseState, firstUpdate: null, lastUpdate: null, firstCapturedUpdate: null, lastCapturedUpdate: null, firstEffect: null, lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, }; return queue; } function cloneUpdateQueue( currentQueue: UpdateQueue, ): UpdateQueue { const queue: UpdateQueue = { baseState: currentQueue.baseState, firstUpdate: currentQueue.firstUpdate, lastUpdate: currentQueue.lastUpdate, // TODO: With resuming, if we bail out and resuse the child tree, we should // keep these effects. firstCapturedUpdate: null, lastCapturedUpdate: null, firstEffect: null, lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, }; return queue; } export function createUpdate( expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, ): Update<*> { let update: Update<*> = { expirationTime, suspenseConfig, tag: UpdateState, payload: null, callback: null, next: null, nextEffect: null, }; if (__DEV__) { update.priority = getCurrentPriorityLevel(); } return update; } function appendUpdateToQueue( queue: UpdateQueue, update: Update, ) { // Append the update to the end of the list. if (queue.lastUpdate === null) { // Queue is empty queue.firstUpdate = queue.lastUpdate = update; } else { queue.lastUpdate.next = update; queue.lastUpdate = update; } } export function enqueueUpdate(fiber: Fiber, update: Update) { // Update queues are created lazily. const alternate = fiber.alternate; let queue1; let queue2; if (alternate === null) { // There's only one fiber. queue1 = fiber.updateQueue; queue2 = null; if (queue1 === null) { queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); } } else { // There are two owners. queue1 = fiber.updateQueue; queue2 = alternate.updateQueue; if (queue1 === null) { if (queue2 === null) { // Neither fiber has an update queue. Create new ones. queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); queue2 = alternate.updateQueue = createUpdateQueue( alternate.memoizedState, ); } else { // Only one fiber has an update queue. Clone to create a new one. queue1 = fiber.updateQueue = cloneUpdateQueue(queue2); } } else { if (queue2 === null) { // Only one fiber has an update queue. Clone to create a new one. queue2 = alternate.updateQueue = cloneUpdateQueue(queue1); } else { // Both owners have an update queue. } } } if (queue2 === null || queue1 === queue2) { // There's only a single queue. appendUpdateToQueue(queue1, update); } else { // There are two queues. We need to append the update to both queues, // while accounting for the persistent structure of the list — we don't // want the same update to be added multiple times. if (queue1.lastUpdate === null || queue2.lastUpdate === null) { // One of the queues is not empty. We must add the update to both queues. appendUpdateToQueue(queue1, update); appendUpdateToQueue(queue2, update); } else { // Both queues are non-empty. The last update is the same in both lists, // because of structural sharing. So, only append to one of the lists. appendUpdateToQueue(queue1, update); // But we still need to update the `lastUpdate` pointer of queue2. queue2.lastUpdate = update; } } if (__DEV__) { if ( fiber.tag === ClassComponent && (currentlyProcessingQueue === queue1 || (queue2 !== null && currentlyProcessingQueue === queue2)) && !didWarnUpdateInsideUpdate ) { warningWithoutStack( false, 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + 'with zero side-effects. Consider using componentDidUpdate or a ' + 'callback.', ); didWarnUpdateInsideUpdate = true; } } } export function enqueueCapturedUpdate( workInProgress: Fiber, update: Update, ) { // Captured updates go into a separate list, and only on the work-in- // progress queue. let workInProgressQueue = workInProgress.updateQueue; if (workInProgressQueue === null) { workInProgressQueue = workInProgress.updateQueue = createUpdateQueue( workInProgress.memoizedState, ); } else { // TODO: I put this here rather than createWorkInProgress so that we don't // clone the queue unnecessarily. There's probably a better way to // structure this. workInProgressQueue = ensureWorkInProgressQueueIsAClone( workInProgress, workInProgressQueue, ); } // Append the update to the end of the list. if (workInProgressQueue.lastCapturedUpdate === null) { // This is the first render phase update workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update; } else { workInProgressQueue.lastCapturedUpdate.next = update; workInProgressQueue.lastCapturedUpdate = update; } } function ensureWorkInProgressQueueIsAClone( workInProgress: Fiber, queue: UpdateQueue, ): UpdateQueue { const current = workInProgress.alternate; if (current !== null) { // If the work-in-progress queue is equal to the current queue, // we need to clone it first. if (queue === current.updateQueue) { queue = workInProgress.updateQueue = cloneUpdateQueue(queue); } } return queue; } function getStateFromUpdate( workInProgress: Fiber, queue: UpdateQueue, update: Update, prevState: State, nextProps: any, instance: any, ): any { switch (update.tag) { case ReplaceState: { const payload = update.payload; if (typeof payload === 'function') { // Updater function if (__DEV__) { enterDisallowedContextReadInDEV(); if ( debugRenderPhaseSideEffects || (debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) ) { payload.call(instance, prevState, nextProps); } } const nextState = payload.call(instance, prevState, nextProps); if (__DEV__) { exitDisallowedContextReadInDEV(); } return nextState; } // State object return payload; } case CaptureUpdate: { workInProgress.effectTag = (workInProgress.effectTag & ~ShouldCapture) | DidCapture; } // Intentional fallthrough case UpdateState: { const payload = update.payload; let partialState; if (typeof payload === 'function') { // Updater function if (__DEV__) { enterDisallowedContextReadInDEV(); if ( debugRenderPhaseSideEffects || (debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) ) { payload.call(instance, prevState, nextProps); } } partialState = payload.call(instance, prevState, nextProps); if (__DEV__) { exitDisallowedContextReadInDEV(); } } else { // Partial state object partialState = payload; } if (partialState === null || partialState === undefined) { // Null and undefined are treated as no-ops. return prevState; } // Merge the partial state and the previous state. return Object.assign({}, prevState, partialState); } case ForceUpdate: { hasForceUpdate = true; return prevState; } } return prevState; } export function processUpdateQueue( workInProgress: Fiber, queue: UpdateQueue, props: any, instance: any, renderExpirationTime: ExpirationTime, ): void { hasForceUpdate = false; queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); if (__DEV__) { currentlyProcessingQueue = queue; } // These values may change as we process the queue. let newBaseState = queue.baseState; let newFirstUpdate = null; let newExpirationTime = NoWork; // Iterate through the list of updates to compute the result. let update = queue.firstUpdate; let resultState = newBaseState; while (update !== null) { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // This update does not have sufficient priority. Skip it. if (newFirstUpdate === null) { // This is the first skipped update. It will be the first update in // the new list. newFirstUpdate = update; // Since this is the first update that was skipped, the current result // is the new base state. newBaseState = resultState; } // Since this update will remain in the list, update the remaining // expiration time. if (newExpirationTime < updateExpirationTime) { newExpirationTime = updateExpirationTime; } } else { // This update does have sufficient priority. // Mark the event time of this update as relevant to this render pass. // TODO: This should ideally use the true event time of this update rather than // its priority which is a derived and not reverseable value. // TODO: We should skip this update if it was already committed but currently // we have no way of detecting the difference between a committed and suspended // update here. markRenderEventTimeAndConfig(updateExpirationTime, update.suspenseConfig); // Process it and compute a new result. resultState = getStateFromUpdate( workInProgress, queue, update, resultState, props, instance, ); const callback = update.callback; if (callback !== null) { workInProgress.effectTag |= Callback; // Set this to null, in case it was mutated during an aborted render. update.nextEffect = null; if (queue.lastEffect === null) { queue.firstEffect = queue.lastEffect = update; } else { queue.lastEffect.nextEffect = update; queue.lastEffect = update; } } } // Continue to the next update. update = update.next; } // Separately, iterate though the list of captured updates. let newFirstCapturedUpdate = null; update = queue.firstCapturedUpdate; while (update !== null) { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // This update does not have sufficient priority. Skip it. if (newFirstCapturedUpdate === null) { // This is the first skipped captured update. It will be the first // update in the new list. newFirstCapturedUpdate = update; // If this is the first update that was skipped, the current result is // the new base state. if (newFirstUpdate === null) { newBaseState = resultState; } } // Since this update will remain in the list, update the remaining // expiration time. if (newExpirationTime < updateExpirationTime) { newExpirationTime = updateExpirationTime; } } else { // This update does have sufficient priority. Process it and compute // a new result. resultState = getStateFromUpdate( workInProgress, queue, update, resultState, props, instance, ); const callback = update.callback; if (callback !== null) { workInProgress.effectTag |= Callback; // Set this to null, in case it was mutated during an aborted render. update.nextEffect = null; if (queue.lastCapturedEffect === null) { queue.firstCapturedEffect = queue.lastCapturedEffect = update; } else { queue.lastCapturedEffect.nextEffect = update; queue.lastCapturedEffect = update; } } } update = update.next; } if (newFirstUpdate === null) { queue.lastUpdate = null; } if (newFirstCapturedUpdate === null) { queue.lastCapturedUpdate = null; } else { workInProgress.effectTag |= Callback; } if (newFirstUpdate === null && newFirstCapturedUpdate === null) { // We processed every update, without skipping. That means the new base // state is the same as the result state. newBaseState = resultState; } queue.baseState = newBaseState; queue.firstUpdate = newFirstUpdate; queue.firstCapturedUpdate = newFirstCapturedUpdate; // Set the remaining expiration time to be whatever is remaining in the queue. // This should be fine because the only two other things that contribute to // expiration time are props and context. We're already in the middle of the // begin phase by the time we start processing the queue, so we've already // dealt with the props. Context in components that specify // shouldComponentUpdate is tricky; but we'll have to account for // that regardless. markUnprocessedUpdateTime(newExpirationTime); workInProgress.expirationTime = newExpirationTime; workInProgress.memoizedState = resultState; if (__DEV__) { currentlyProcessingQueue = null; } } function callCallback(callback, context) { invariant( typeof callback === 'function', 'Invalid argument passed as callback. Expected a function. Instead ' + 'received: %s', callback, ); callback.call(context); } export function resetHasForceUpdateBeforeProcessing() { hasForceUpdate = false; } export function checkHasForceUpdateAfterProcessing(): boolean { return hasForceUpdate; } export function commitUpdateQueue( finishedWork: Fiber, finishedQueue: UpdateQueue, instance: any, renderExpirationTime: ExpirationTime, ): void { // If the finished render included captured updates, and there are still // lower priority updates left over, we need to keep the captured updates // in the queue so that they are rebased and not dropped once we process the // queue again at the lower priority. if (finishedQueue.firstCapturedUpdate !== null) { // Join the captured update list to the end of the normal list. if (finishedQueue.lastUpdate !== null) { finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate; finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate; } // Clear the list of captured updates. finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null; } // Commit the effects commitUpdateEffects(finishedQueue.firstEffect, instance); finishedQueue.firstEffect = finishedQueue.lastEffect = null; commitUpdateEffects(finishedQueue.firstCapturedEffect, instance); finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null; } function commitUpdateEffects( effect: Update | null, instance: any, ): void { while (effect !== null) { const callback = effect.callback; if (callback !== null) { effect.callback = null; callCallback(callback, instance); } effect = effect.nextEffect; } }