| 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 | } |