746 lines
25 KiB
JavaScript
746 lines
25 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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, FiberRoot} from './ReactInternalTypes';
|
|
import type {Lanes, Lane} from './ReactFiberLane';
|
|
|
|
import {
|
|
NoLane,
|
|
NoLanes,
|
|
OffscreenLane,
|
|
isSubsetOfLanes,
|
|
mergeLanes,
|
|
removeLanes,
|
|
isTransitionLane,
|
|
intersectLanes,
|
|
markRootEntangled,
|
|
} from './ReactFiberLane';
|
|
import {
|
|
enterDisallowedContextReadInDEV,
|
|
exitDisallowedContextReadInDEV,
|
|
} from './ReactFiberNewContext';
|
|
import {
|
|
Callback,
|
|
Visibility,
|
|
ShouldCapture,
|
|
DidCapture,
|
|
} from './ReactFiberFlags';
|
|
import getComponentNameFromFiber from './getComponentNameFromFiber';
|
|
|
|
import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';
|
|
|
|
import {StrictLegacyMode} from './ReactTypeOfMode';
|
|
import {
|
|
markSkippedUpdateLanes,
|
|
isUnsafeClassRenderPhaseUpdate,
|
|
getWorkInProgressRootRenderLanes,
|
|
} from './ReactFiberWorkLoop';
|
|
import {
|
|
enqueueConcurrentClassUpdate,
|
|
unsafe_markUpdateLaneFromFiberToRoot,
|
|
} from './ReactFiberConcurrentUpdates';
|
|
import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook';
|
|
|
|
import assign from 'shared/assign';
|
|
|
|
export type Update<State> = {
|
|
// TODO: Temporary field. Will remove this by storing a map of
|
|
// transition -> event time on the root.
|
|
eventTime: number,
|
|
lane: Lane,
|
|
|
|
tag: 0 | 1 | 2 | 3,
|
|
payload: any,
|
|
callback: (() => mixed) | null,
|
|
|
|
next: Update<State> | null,
|
|
};
|
|
|
|
export type SharedQueue<State> = {
|
|
pending: Update<State> | null,
|
|
lanes: Lanes,
|
|
hiddenCallbacks: Array<() => mixed> | null,
|
|
};
|
|
|
|
export type UpdateQueue<State> = {
|
|
baseState: State,
|
|
firstBaseUpdate: Update<State> | null,
|
|
lastBaseUpdate: Update<State> | null,
|
|
shared: SharedQueue<State>,
|
|
callbacks: Array<() => mixed> | 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: () => void;
|
|
if (__DEV__) {
|
|
didWarnUpdateInsideUpdate = false;
|
|
currentlyProcessingQueue = null;
|
|
resetCurrentlyProcessingQueue = () => {
|
|
currentlyProcessingQueue = null;
|
|
};
|
|
}
|
|
|
|
export function initializeUpdateQueue<State>(fiber: Fiber): void {
|
|
const queue: UpdateQueue<State> = {
|
|
baseState: fiber.memoizedState,
|
|
firstBaseUpdate: null,
|
|
lastBaseUpdate: null,
|
|
shared: {
|
|
pending: null,
|
|
lanes: NoLanes,
|
|
hiddenCallbacks: null,
|
|
},
|
|
callbacks: null,
|
|
};
|
|
fiber.updateQueue = queue;
|
|
}
|
|
|
|
export function cloneUpdateQueue<State>(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
): void {
|
|
// Clone the update queue from current. Unless it's already a clone.
|
|
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
|
|
const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
|
|
if (queue === currentQueue) {
|
|
const clone: UpdateQueue<State> = {
|
|
baseState: currentQueue.baseState,
|
|
firstBaseUpdate: currentQueue.firstBaseUpdate,
|
|
lastBaseUpdate: currentQueue.lastBaseUpdate,
|
|
shared: currentQueue.shared,
|
|
callbacks: null,
|
|
};
|
|
workInProgress.updateQueue = clone;
|
|
}
|
|
}
|
|
|
|
export function createUpdate(eventTime: number, lane: Lane): Update<mixed> {
|
|
const update: Update<mixed> = {
|
|
eventTime,
|
|
lane,
|
|
|
|
tag: UpdateState,
|
|
payload: null,
|
|
callback: null,
|
|
|
|
next: null,
|
|
};
|
|
return update;
|
|
}
|
|
|
|
export function enqueueUpdate<State>(
|
|
fiber: Fiber,
|
|
update: Update<State>,
|
|
lane: Lane,
|
|
): FiberRoot | null {
|
|
const updateQueue = fiber.updateQueue;
|
|
if (updateQueue === null) {
|
|
// Only occurs if the fiber has been unmounted.
|
|
return null;
|
|
}
|
|
|
|
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
|
|
|
|
if (__DEV__) {
|
|
if (
|
|
currentlyProcessingQueue === sharedQueue &&
|
|
!didWarnUpdateInsideUpdate
|
|
) {
|
|
const componentName = getComponentNameFromFiber(fiber);
|
|
console.error(
|
|
'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.\n\nPlease update the following component: %s',
|
|
componentName,
|
|
);
|
|
didWarnUpdateInsideUpdate = true;
|
|
}
|
|
}
|
|
|
|
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
|
|
// This is an unsafe render phase update. Add directly to the update
|
|
// queue so we can process it immediately during the current render.
|
|
const pending = sharedQueue.pending;
|
|
if (pending === null) {
|
|
// This is the first update. Create a circular list.
|
|
update.next = update;
|
|
} else {
|
|
update.next = pending.next;
|
|
pending.next = update;
|
|
}
|
|
sharedQueue.pending = update;
|
|
|
|
// Update the childLanes even though we're most likely already rendering
|
|
// this fiber. This is for backwards compatibility in the case where you
|
|
// update a different component during render phase than the one that is
|
|
// currently renderings (a pattern that is accompanied by a warning).
|
|
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
|
|
} else {
|
|
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
|
|
}
|
|
}
|
|
|
|
export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
|
|
const updateQueue = fiber.updateQueue;
|
|
if (updateQueue === null) {
|
|
// Only occurs if the fiber has been unmounted.
|
|
return;
|
|
}
|
|
|
|
const sharedQueue: SharedQueue<mixed> = (updateQueue: any).shared;
|
|
if (isTransitionLane(lane)) {
|
|
let queueLanes = sharedQueue.lanes;
|
|
|
|
// If any entangled lanes are no longer pending on the root, then they must
|
|
// have finished. We can remove them from the shared queue, which represents
|
|
// a superset of the actually pending lanes. In some cases we may entangle
|
|
// more than we need to, but that's OK. In fact it's worse if we *don't*
|
|
// entangle when we should.
|
|
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
|
|
|
|
// Entangle the new transition lane with the other transition lanes.
|
|
const newQueueLanes = mergeLanes(queueLanes, lane);
|
|
sharedQueue.lanes = newQueueLanes;
|
|
// Even if queue.lanes already include lane, we don't know for certain if
|
|
// the lane finished since the last time we entangled it. So we need to
|
|
// entangle it again, just to be sure.
|
|
markRootEntangled(root, newQueueLanes);
|
|
}
|
|
}
|
|
|
|
export function enqueueCapturedUpdate<State>(
|
|
workInProgress: Fiber,
|
|
capturedUpdate: Update<State>,
|
|
) {
|
|
// Captured updates are updates that are thrown by a child during the render
|
|
// phase. They should be discarded if the render is aborted. Therefore,
|
|
// we should only put them on the work-in-progress queue, not the current one.
|
|
let queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
|
|
|
|
// Check if the work-in-progress queue is a clone.
|
|
const current = workInProgress.alternate;
|
|
if (current !== null) {
|
|
const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
|
|
if (queue === currentQueue) {
|
|
// The work-in-progress queue is the same as current. This happens when
|
|
// we bail out on a parent fiber that then captures an error thrown by
|
|
// a child. Since we want to append the update only to the work-in
|
|
// -progress queue, we need to clone the updates. We usually clone during
|
|
// processUpdateQueue, but that didn't happen in this case because we
|
|
// skipped over the parent when we bailed out.
|
|
let newFirst = null;
|
|
let newLast = null;
|
|
const firstBaseUpdate = queue.firstBaseUpdate;
|
|
if (firstBaseUpdate !== null) {
|
|
// Loop through the updates and clone them.
|
|
let update: Update<State> = firstBaseUpdate;
|
|
do {
|
|
const clone: Update<State> = {
|
|
eventTime: update.eventTime,
|
|
lane: update.lane,
|
|
|
|
tag: update.tag,
|
|
payload: update.payload,
|
|
// When this update is rebased, we should not fire its
|
|
// callback again.
|
|
callback: null,
|
|
|
|
next: null,
|
|
};
|
|
if (newLast === null) {
|
|
newFirst = newLast = clone;
|
|
} else {
|
|
newLast.next = clone;
|
|
newLast = clone;
|
|
}
|
|
// $FlowFixMe[incompatible-type] we bail out when we get a null
|
|
update = update.next;
|
|
} while (update !== null);
|
|
|
|
// Append the captured update the end of the cloned list.
|
|
if (newLast === null) {
|
|
newFirst = newLast = capturedUpdate;
|
|
} else {
|
|
newLast.next = capturedUpdate;
|
|
newLast = capturedUpdate;
|
|
}
|
|
} else {
|
|
// There are no base updates.
|
|
newFirst = newLast = capturedUpdate;
|
|
}
|
|
queue = {
|
|
baseState: currentQueue.baseState,
|
|
firstBaseUpdate: newFirst,
|
|
lastBaseUpdate: newLast,
|
|
shared: currentQueue.shared,
|
|
callbacks: currentQueue.callbacks,
|
|
};
|
|
workInProgress.updateQueue = queue;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Append the update to the end of the list.
|
|
const lastBaseUpdate = queue.lastBaseUpdate;
|
|
if (lastBaseUpdate === null) {
|
|
queue.firstBaseUpdate = capturedUpdate;
|
|
} else {
|
|
lastBaseUpdate.next = capturedUpdate;
|
|
}
|
|
queue.lastBaseUpdate = capturedUpdate;
|
|
}
|
|
|
|
function getStateFromUpdate<State>(
|
|
workInProgress: Fiber,
|
|
queue: UpdateQueue<State>,
|
|
update: Update<State>,
|
|
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();
|
|
}
|
|
const nextState = payload.call(instance, prevState, nextProps);
|
|
if (__DEV__) {
|
|
if (
|
|
debugRenderPhaseSideEffectsForStrictMode &&
|
|
workInProgress.mode & StrictLegacyMode
|
|
) {
|
|
setIsStrictModeForDevtools(true);
|
|
try {
|
|
payload.call(instance, prevState, nextProps);
|
|
} finally {
|
|
setIsStrictModeForDevtools(false);
|
|
}
|
|
}
|
|
exitDisallowedContextReadInDEV();
|
|
}
|
|
return nextState;
|
|
}
|
|
// State object
|
|
return payload;
|
|
}
|
|
case CaptureUpdate: {
|
|
workInProgress.flags =
|
|
(workInProgress.flags & ~ShouldCapture) | DidCapture;
|
|
}
|
|
// Intentional fallthrough
|
|
case UpdateState: {
|
|
const payload = update.payload;
|
|
let partialState;
|
|
if (typeof payload === 'function') {
|
|
// Updater function
|
|
if (__DEV__) {
|
|
enterDisallowedContextReadInDEV();
|
|
}
|
|
partialState = payload.call(instance, prevState, nextProps);
|
|
if (__DEV__) {
|
|
if (
|
|
debugRenderPhaseSideEffectsForStrictMode &&
|
|
workInProgress.mode & StrictLegacyMode
|
|
) {
|
|
setIsStrictModeForDevtools(true);
|
|
try {
|
|
payload.call(instance, prevState, nextProps);
|
|
} finally {
|
|
setIsStrictModeForDevtools(false);
|
|
}
|
|
}
|
|
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 assign({}, prevState, partialState);
|
|
}
|
|
case ForceUpdate: {
|
|
hasForceUpdate = true;
|
|
return prevState;
|
|
}
|
|
}
|
|
return prevState;
|
|
}
|
|
|
|
export function processUpdateQueue<State>(
|
|
workInProgress: Fiber,
|
|
props: any,
|
|
instance: any,
|
|
renderLanes: Lanes,
|
|
): void {
|
|
// This is always non-null on a ClassComponent or HostRoot
|
|
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
|
|
|
|
hasForceUpdate = false;
|
|
|
|
if (__DEV__) {
|
|
// $FlowFixMe[escaped-generic] discovered when updating Flow
|
|
currentlyProcessingQueue = queue.shared;
|
|
}
|
|
|
|
let firstBaseUpdate = queue.firstBaseUpdate;
|
|
let lastBaseUpdate = queue.lastBaseUpdate;
|
|
|
|
// Check if there are pending updates. If so, transfer them to the base queue.
|
|
let pendingQueue = queue.shared.pending;
|
|
if (pendingQueue !== null) {
|
|
queue.shared.pending = null;
|
|
|
|
// The pending queue is circular. Disconnect the pointer between first
|
|
// and last so that it's non-circular.
|
|
const lastPendingUpdate = pendingQueue;
|
|
const firstPendingUpdate = lastPendingUpdate.next;
|
|
lastPendingUpdate.next = null;
|
|
// Append pending updates to base queue
|
|
if (lastBaseUpdate === null) {
|
|
firstBaseUpdate = firstPendingUpdate;
|
|
} else {
|
|
lastBaseUpdate.next = firstPendingUpdate;
|
|
}
|
|
lastBaseUpdate = lastPendingUpdate;
|
|
|
|
// If there's a current queue, and it's different from the base queue, then
|
|
// we need to transfer the updates to that queue, too. Because the base
|
|
// queue is a singly-linked list with no cycles, we can append to both
|
|
// lists and take advantage of structural sharing.
|
|
// TODO: Pass `current` as argument
|
|
const current = workInProgress.alternate;
|
|
if (current !== null) {
|
|
// This is always non-null on a ClassComponent or HostRoot
|
|
const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
|
|
const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
|
|
if (currentLastBaseUpdate !== lastBaseUpdate) {
|
|
if (currentLastBaseUpdate === null) {
|
|
currentQueue.firstBaseUpdate = firstPendingUpdate;
|
|
} else {
|
|
currentLastBaseUpdate.next = firstPendingUpdate;
|
|
}
|
|
currentQueue.lastBaseUpdate = lastPendingUpdate;
|
|
}
|
|
}
|
|
}
|
|
|
|
// These values may change as we process the queue.
|
|
if (firstBaseUpdate !== null) {
|
|
// Iterate through the list of updates to compute the result.
|
|
let newState = queue.baseState;
|
|
// TODO: Don't need to accumulate this. Instead, we can remove renderLanes
|
|
// from the original lanes.
|
|
let newLanes = NoLanes;
|
|
|
|
let newBaseState = null;
|
|
let newFirstBaseUpdate = null;
|
|
let newLastBaseUpdate = null;
|
|
|
|
let update: Update<State> = firstBaseUpdate;
|
|
do {
|
|
// TODO: Don't need this field anymore
|
|
const updateEventTime = update.eventTime;
|
|
|
|
// An extra OffscreenLane bit is added to updates that were made to
|
|
// a hidden tree, so that we can distinguish them from updates that were
|
|
// already there when the tree was hidden.
|
|
const updateLane = removeLanes(update.lane, OffscreenLane);
|
|
const isHiddenUpdate = updateLane !== update.lane;
|
|
|
|
// Check if this update was made while the tree was hidden. If so, then
|
|
// it's not a "base" update and we should disregard the extra base lanes
|
|
// that were added to renderLanes when we entered the Offscreen tree.
|
|
const shouldSkipUpdate = isHiddenUpdate
|
|
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
|
|
: !isSubsetOfLanes(renderLanes, updateLane);
|
|
|
|
if (shouldSkipUpdate) {
|
|
// Priority is insufficient. Skip this update. If this is the first
|
|
// skipped update, the previous update/state is the new base
|
|
// update/state.
|
|
const clone: Update<State> = {
|
|
eventTime: updateEventTime,
|
|
lane: updateLane,
|
|
|
|
tag: update.tag,
|
|
payload: update.payload,
|
|
callback: update.callback,
|
|
|
|
next: null,
|
|
};
|
|
if (newLastBaseUpdate === null) {
|
|
newFirstBaseUpdate = newLastBaseUpdate = clone;
|
|
newBaseState = newState;
|
|
} else {
|
|
newLastBaseUpdate = newLastBaseUpdate.next = clone;
|
|
}
|
|
// Update the remaining priority in the queue.
|
|
newLanes = mergeLanes(newLanes, updateLane);
|
|
} else {
|
|
// This update does have sufficient priority.
|
|
|
|
if (newLastBaseUpdate !== null) {
|
|
const clone: Update<State> = {
|
|
eventTime: updateEventTime,
|
|
// This update is going to be committed so we never want uncommit
|
|
// it. Using NoLane works because 0 is a subset of all bitmasks, so
|
|
// this will never be skipped by the check above.
|
|
lane: NoLane,
|
|
|
|
tag: update.tag,
|
|
payload: update.payload,
|
|
|
|
// When this update is rebased, we should not fire its
|
|
// callback again.
|
|
callback: null,
|
|
|
|
next: null,
|
|
};
|
|
newLastBaseUpdate = newLastBaseUpdate.next = clone;
|
|
}
|
|
|
|
// Process this update.
|
|
newState = getStateFromUpdate(
|
|
workInProgress,
|
|
queue,
|
|
update,
|
|
newState,
|
|
props,
|
|
instance,
|
|
);
|
|
const callback = update.callback;
|
|
if (callback !== null) {
|
|
workInProgress.flags |= Callback;
|
|
if (isHiddenUpdate) {
|
|
workInProgress.flags |= Visibility;
|
|
}
|
|
const callbacks = queue.callbacks;
|
|
if (callbacks === null) {
|
|
queue.callbacks = [callback];
|
|
} else {
|
|
callbacks.push(callback);
|
|
}
|
|
}
|
|
}
|
|
// $FlowFixMe[incompatible-type] we bail out when we get a null
|
|
update = update.next;
|
|
if (update === null) {
|
|
pendingQueue = queue.shared.pending;
|
|
if (pendingQueue === null) {
|
|
break;
|
|
} else {
|
|
// An update was scheduled from inside a reducer. Add the new
|
|
// pending updates to the end of the list and keep processing.
|
|
const lastPendingUpdate = pendingQueue;
|
|
// Intentionally unsound. Pending updates form a circular list, but we
|
|
// unravel them when transferring them to the base queue.
|
|
const firstPendingUpdate =
|
|
((lastPendingUpdate.next: any): Update<State>);
|
|
lastPendingUpdate.next = null;
|
|
update = firstPendingUpdate;
|
|
queue.lastBaseUpdate = lastPendingUpdate;
|
|
queue.shared.pending = null;
|
|
}
|
|
}
|
|
} while (true);
|
|
|
|
if (newLastBaseUpdate === null) {
|
|
newBaseState = newState;
|
|
}
|
|
|
|
queue.baseState = ((newBaseState: any): State);
|
|
queue.firstBaseUpdate = newFirstBaseUpdate;
|
|
queue.lastBaseUpdate = newLastBaseUpdate;
|
|
|
|
if (firstBaseUpdate === null) {
|
|
// `queue.lanes` is used for entangling transitions. We can set it back to
|
|
// zero once the queue is empty.
|
|
queue.shared.lanes = NoLanes;
|
|
}
|
|
|
|
// 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.
|
|
markSkippedUpdateLanes(newLanes);
|
|
workInProgress.lanes = newLanes;
|
|
workInProgress.memoizedState = newState;
|
|
}
|
|
|
|
if (__DEV__) {
|
|
currentlyProcessingQueue = null;
|
|
}
|
|
}
|
|
|
|
function callCallback(callback: () => mixed, context: any) {
|
|
if (typeof callback !== 'function') {
|
|
throw new Error(
|
|
'Invalid argument passed as callback. Expected a function. Instead ' +
|
|
`received: ${callback}`,
|
|
);
|
|
}
|
|
|
|
callback.call(context);
|
|
}
|
|
|
|
export function resetHasForceUpdateBeforeProcessing() {
|
|
hasForceUpdate = false;
|
|
}
|
|
|
|
export function checkHasForceUpdateAfterProcessing(): boolean {
|
|
return hasForceUpdate;
|
|
}
|
|
|
|
export function deferHiddenCallbacks<State>(
|
|
updateQueue: UpdateQueue<State>,
|
|
): void {
|
|
// When an update finishes on a hidden component, its callback should not
|
|
// be fired until/unless the component is made visible again. Stash the
|
|
// callback on the shared queue object so it can be fired later.
|
|
const newHiddenCallbacks = updateQueue.callbacks;
|
|
if (newHiddenCallbacks !== null) {
|
|
const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;
|
|
if (existingHiddenCallbacks === null) {
|
|
updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;
|
|
} else {
|
|
updateQueue.shared.hiddenCallbacks =
|
|
existingHiddenCallbacks.concat(newHiddenCallbacks);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function commitHiddenCallbacks<State>(
|
|
updateQueue: UpdateQueue<State>,
|
|
context: any,
|
|
): void {
|
|
// This component is switching from hidden -> visible. Commit any callbacks
|
|
// that were previously deferred.
|
|
const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;
|
|
if (hiddenCallbacks !== null) {
|
|
updateQueue.shared.hiddenCallbacks = null;
|
|
for (let i = 0; i < hiddenCallbacks.length; i++) {
|
|
const callback = hiddenCallbacks[i];
|
|
callCallback(callback, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function commitCallbacks<State>(
|
|
updateQueue: UpdateQueue<State>,
|
|
context: any,
|
|
): void {
|
|
const callbacks = updateQueue.callbacks;
|
|
if (callbacks !== null) {
|
|
updateQueue.callbacks = null;
|
|
for (let i = 0; i < callbacks.length; i++) {
|
|
const callback = callbacks[i];
|
|
callCallback(callback, context);
|
|
}
|
|
}
|
|
}
|