React 状态更新中的双缓冲机制、优先级调度

文章主要解释了 React 如何处理状态更新,特别是其双缓冲机制优先级调度

文章内容结合 deepseek 进行了汇总

一、UpdateQueue 的结构与双缓冲机制

1. 更新队列是一个链表

  • 每个 Update 代表一个状态更新(如 setState)。
  • 队列按插入顺序存储更新,而非按优先级排序。

2. 双队列设计:current 与 work-in-progress

React 为每个组件维护两个队列:

  • current queue:对应已提交到 DOM 的当前状态。
  • work-in-progress queue:对应正在进行的渲染状态,可异步计算。

关键行为:

  • 当开始一次新的渲染时,会克隆 current queue 作为 work-in-progress queue 的初始值。
  • 当提交(commit)时,work-in-progress queue 会成为新的 current queue。
  • 如果渲染被中断并丢弃,则基于 current queue 重新创建 work-in-progress queue。

3. 为什么更新要同时追加到两个队列?

  • 如果只追加到 work-in-progress queue,当渲染被丢弃并重新克隆 current queue 时,新更新会丢失。

  • 如果只追加到 current queue,当 work-in-progress queue 提交并覆盖 current queue 时,新更新也会丢失。

  • 同时追加到两个队列保证更新一定会被下一次渲染处理,且不会重复应用。

二、优先级处理机制

1. 更新按插入顺序存储,但按优先级处理

更新在链表中按插入顺序排列,不按优先级排序。

处理队列时,只处理优先级足够的更新。

如果某个更新因优先级不足被跳过,它之后的所有更新都会保留,即使它们优先级足够。

2. base state 的作用

  • base state 是队列中第一个更新之前的状态。

  • 当跳过某些更新时,后续的高优先级更新会基于新的 base state 重新计算(rebase)。

3. 举例说明(注释中的例子):

初始状态:''

更新队列(字母为更新内容,数字为优先级):

复制代码
A1 - B2 - C1 - D2
第一次渲染(优先级 1):
  • base state: ''

  • 可处理的更新:A1(优先级1)、C1(优先级1)

  • 跳过 B2(优先级不足)

  • 结果状态:'AC'

  • 注意:C1 是基于 'A' 状态处理的,但 B2 被跳过,所以 B2 和 D2 留在队列中。

第二次渲染(优先级 2):
  • base state: 'A'(跳过的 B2 之前的状态)

  • 队列中剩余更新:B2、C1、D2

  • 可处理的更新:B2、C1、D2(优先级都满足)

  • 结果状态:'ABCD'

关键点:
  • C1 被处理了两次(在两个优先级下),但最终状态是一致的。
  • 最终结果与按顺序同步处理所有更新相同('ABCD')。

三、设计哲学与优势

1. 确定性最终状态

  • 无论更新优先级如何、中间是否被中断,最终状态总是与同步顺序处理所有更新一致。
  • 这是 React 可预测性的核心保证。

2. 时间切片与并发渲染的支撑

  • 允许高优先级更新打断低优先级渲染。
  • 被跳过的更新和后续更新会被保留,在更低优先级时处理。

3. 性能与响应性平衡

  • 高优先级更新(如用户输入)可快速响应。
  • 低优先级更新(如数据拉取)可等待或被打断。

四、实际应用中的体现

这种机制在 React 特性中体现为:

  • 自动批处理:多个 setState 合并为一个更新。
  • 并发特性(Concurrent Mode):高优先级更新可打断低优先级渲染。
  • Suspense:数据获取更新可被推迟。

五、简单总结

特点 说明
双缓冲队列 current(已提交)和 work-in-progress(计算中),保证更新不丢失。
插入顺序存储 更新按调用顺序追加到链表尾部,不按优先级排序。
优先级筛选处理 处理时跳过优先级不足的更新,但保留后续所有更新。
base state 重定基 跳过更新时,后续更新基于新的 base state 重新计算。。
确定性结果 最终状态与同步处理所有更新一致,中间状态可能不同。。

这种设计让 React 在异步、可中断的渲染过程中,既能保证最终状态的一致性,又能实现优先级调度,优化用户体验。

附\] ReactUpdateQueue.js 代码如下: ```js /** * 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 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/ReactSideEffectTags'; import {ClassComponent} from 'shared/ReactWorkTags'; import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, } from 'shared/ReactFeatureFlags'; import {StrictMode} from './ReactTypeOfMode'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; export type Update = { expirationTime: ExpirationTime, tag: 0 | 1 | 2 | 3, payload: any, callback: (() => mixed) | null, next: Update | null, nextEffect: Update | null, }; export type UpdateQueue = { baseState: State, firstUpdate: Update | null, lastUpdate: Update | null, firstCapturedUpdate: Update | null, // ErrorBoundary 异常捕获使用 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): Update<*> { return { expirationTime: expirationTime, tag: UpdateState, payload: null, callback: null, next: null, nextEffect: null, }; } 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__) { 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: { 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 === 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; // 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. 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; } } ``` 至此,结束。

相关推荐
nnnnna6 小时前
插槽(Slots)(完整详细版)
前端·vue.js
游戏开发爱好者86 小时前
H5 混合应用加密 Web 资源暴露到 IPA 层防护的完整技术方案
android·前端·ios·小程序·uni-app·iphone·webview
hayzone6 小时前
pnpm 你用了吗?
前端
hellsing6 小时前
UniPush 2.0 实战指南:工单提醒与多厂商通道配置
前端·javascript
快手技术6 小时前
入围AA总榜Top 10,Non-Reasoning Model榜单第一!KAT-Coder-Pro V1 新版本踏浪归来!
前端·后端·前端框架
wangpq6 小时前
记录曾经打开半屏小程序遇到的事
前端·微信小程序
king王一帅6 小时前
告别 AI 输出的重复解析:正常 markdown 解析渲染也能提速 2-10 倍以上
前端·javascript·ai编程
dudke6 小时前
js的reduce详解
开发语言·javascript·ecmascript
huangql5206 小时前
网络体系结构在Web前端性能优化中的应用完全指南
前端·性能优化