/** * Copyright (c) 2013-present, Facebook, Inc. * * 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 is // 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 {NoWork} from './ReactFiberExpirationTime'; import { Callback, ShouldCapture, DidCapture, } from 'shared/ReactTypeOfSideEffect'; import {ClassComponent} from 'shared/ReactTypeOfWork'; import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, } from 'shared/ReactFeatureFlags'; import {StrictMode} from './ReactTypeOfMode'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; export type Update = { expirationTime: ExpirationTime, tag: 0 | 1 | 2 | 3, payload: any, callback: (() => mixed) | null, next: Update | null, nextEffect: Update | null, }; export type UpdateQueue = { expirationTime: ExpirationTime, 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, // TODO: Workaround for lack of tuples. Could use global state instead. hasForceUpdate: boolean, }; export const UpdateState = 0; export const ReplaceState = 1; export const ForceUpdate = 2; export const CaptureUpdate = 3; 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 = { expirationTime: NoWork, baseState, firstUpdate: null, lastUpdate: null, firstCapturedUpdate: null, lastCapturedUpdate: null, firstEffect: null, lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, hasForceUpdate: false, }; return queue; } function cloneUpdateQueue( currentQueue: UpdateQueue, ): UpdateQueue { const queue: UpdateQueue = { expirationTime: currentQueue.expirationTime, 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, hasForceUpdate: false, firstEffect: null, lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, }; return queue; } export function createUpdate(expirationTime: ExpirationTime): Update<*> { return { expirationTime: expirationTime, tag: UpdateState, payload: null, callback: null, next: null, nextEffect: null, }; } function appendUpdateToQueue( queue: UpdateQueue, update: Update, expirationTime: ExpirationTime, ) { // 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; } if ( queue.expirationTime === NoWork || queue.expirationTime > expirationTime ) { // The incoming update has the earliest expiration of any update in the // queue. Update the queue's expiration time. queue.expirationTime = expirationTime; } } export function enqueueUpdate( fiber: Fiber, update: Update, expirationTime: ExpirationTime, ) { // 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, expirationTime); } 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, expirationTime); appendUpdateToQueue(queue2, update, expirationTime); } 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, expirationTime); // 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 ) { warning( 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, renderExpirationTime: ExpirationTime, ) { // 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; } if ( workInProgressQueue.expirationTime === NoWork || workInProgressQueue.expirationTime > renderExpirationTime ) { // The incoming update has the earliest expiration of any update in the // queue. Update the queue's expiration time. workInProgressQueue.expirationTime = renderExpirationTime; } } 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__) { if ( debugRenderPhaseSideEffects || (debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) ) { payload.call(instance, prevState, nextProps); } } return payload.call(instance, prevState, nextProps); } // 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__) { if ( debugRenderPhaseSideEffects || (debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) ) { payload.call(instance, prevState, nextProps); } } partialState = payload.call(instance, prevState, nextProps); } 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: { queue.hasForceUpdate = true; return prevState; } } return prevState; } export function processUpdateQueue( workInProgress: Fiber, queue: UpdateQueue, props: any, instance: any, renderExpirationTime: ExpirationTime, ): void { if ( queue.expirationTime === NoWork || queue.expirationTime > renderExpirationTime ) { // Insufficient priority. Bailout. return; } 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 === NoWork || 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.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 === NoWork || 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; queue.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 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 let effect = finishedQueue.firstEffect; finishedQueue.firstEffect = finishedQueue.lastEffect = null; while (effect !== null) { const callback = effect.callback; if (callback !== null) { effect.callback = null; callCallback(callback, instance); } effect = effect.nextEffect; } effect = finishedQueue.firstCapturedEffect; finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null; while (effect !== null) { const callback = effect.callback; if (callback !== null) { effect.callback = null; callCallback(callback, instance); } effect = effect.nextEffect; } }