news 2025/12/19 22:40:35

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 代码如下:

/** * 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.importtype{Fiber}from'./ReactFiber';importtype{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';importinvariantfrom'shared/invariant';importwarningWithoutStackfrom'shared/warningWithoutStack';exporttype Update<State>={expirationTime:ExpirationTime,tag:0|1|2|3,payload:any,callback:(()=>mixed)|null,next:Update<State>|null,nextEffect:Update<State>|null,};exporttype UpdateQueue<State>={baseState:State,firstUpdate:Update<State>|null,lastUpdate:Update<State>|null,firstCapturedUpdate:Update<State>|null,// ErrorBoundary 异常捕获使用lastCapturedUpdate:Update<State>|null,firstEffect:Update<State>|null,lastEffect:Update<State>|null,firstCapturedEffect:Update<State>|null,lastCapturedEffect:Update<State>|null,};exportconstUpdateState=0;exportconstReplaceState=1;exportconstForceUpdate=2;exportconstCaptureUpdate=3;// Global state that is reset at the beginning of calling `processUpdateQueue`.// It should only be read right after calling `processUpdateQueue`, via// `checkHasForceUpdateAfterProcessing`.lethasForceUpdate=false;letdidWarnUpdateInsideUpdate;letcurrentlyProcessingQueue;exportletresetCurrentlyProcessingQueue;if(__DEV__){didWarnUpdateInsideUpdate=false;currentlyProcessingQueue=null;resetCurrentlyProcessingQueue=()=>{currentlyProcessingQueue=null;};}exportfunctioncreateUpdateQueue<State>(baseState:State):UpdateQueue<State>{constqueue:UpdateQueue<State>={baseState,firstUpdate:null,lastUpdate:null,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null,};returnqueue;}functioncloneUpdateQueue<State>(currentQueue:UpdateQueue<State>,):UpdateQueue<State>{constqueue:UpdateQueue<State>={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,};returnqueue;}exportfunctioncreateUpdate(expirationTime:ExpirationTime):Update<*>{return{expirationTime:expirationTime,tag:UpdateState,payload:null,callback:null,next:null,nextEffect:null,};}functionappendUpdateToQueue<State>(queue:UpdateQueue<State>,update:Update<State>,){// Append the update to the end of the list.if(queue.lastUpdate===null){// Queue is emptyqueue.firstUpdate=queue.lastUpdate=update;}else{queue.lastUpdate.next=update;queue.lastUpdate=update;}}exportfunctionenqueueUpdate<State>(fiber:Fiber,update:Update<State>){// Update queues are created lazily.constalternate=fiber.alternate;letqueue1;letqueue2;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;}}}exportfunctionenqueueCapturedUpdate<State>(workInProgress:Fiber,update:Update<State>,){// Captured updates go into a separate list, and only on the work-in-// progress queue.letworkInProgressQueue=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 updateworkInProgressQueue.firstCapturedUpdate=workInProgressQueue.lastCapturedUpdate=update;}else{workInProgressQueue.lastCapturedUpdate.next=update;workInProgressQueue.lastCapturedUpdate=update;}}functionensureWorkInProgressQueueIsAClone<State>(workInProgress:Fiber,queue:UpdateQueue<State>,):UpdateQueue<State>{constcurrent=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);}}returnqueue;}functiongetStateFromUpdate<State>(workInProgress:Fiber,queue:UpdateQueue<State>,update:Update<State>,prevState:State,nextProps:any,instance:any,):any{switch(update.tag){caseReplaceState:{constpayload=update.payload;if(typeofpayload==='function'){// Updater functionif(__DEV__){if(debugRenderPhaseSideEffects||(debugRenderPhaseSideEffectsForStrictMode&&workInProgress.mode&StrictMode)){payload.call(instance,prevState,nextProps);}}returnpayload.call(instance,prevState,nextProps);}// State objectreturnpayload;}caseCaptureUpdate:{workInProgress.effectTag=(workInProgress.effectTag&~ShouldCapture)|DidCapture;}// Intentional fallthroughcaseUpdateState:{constpayload=update.payload;letpartialState;if(typeofpayload==='function'){// Updater functionif(__DEV__){if(debugRenderPhaseSideEffects||(debugRenderPhaseSideEffectsForStrictMode&&workInProgress.mode&StrictMode)){payload.call(instance,prevState,nextProps);}}partialState=payload.call(instance,prevState,nextProps);}else{// Partial state objectpartialState=payload;}if(partialState===null||partialState===undefined){// Null and undefined are treated as no-ops.returnprevState;}// Merge the partial state and the previous state.returnObject.assign({},prevState,partialState);}caseForceUpdate:{hasForceUpdate=true;returnprevState;}}returnprevState;}exportfunctionprocessUpdateQueue<State>(workInProgress:Fiber,queue:UpdateQueue<State>,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.letnewBaseState=queue.baseState;letnewFirstUpdate=null;letnewExpirationTime=NoWork;// Iterate through the list of updates to compute the result.letupdate=queue.firstUpdate;letresultState=newBaseState;while(update!==null){constupdateExpirationTime=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,);constcallback=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.letnewFirstCapturedUpdate=null;update=queue.firstCapturedUpdate;while(update!==null){constupdateExpirationTime=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,);constcallback=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;}}functioncallCallback(callback,context){invariant(typeofcallback==='function','Invalid argument passed as callback. Expected a function. Instead '+'received: %s',callback,);callback.call(context);}exportfunctionresetHasForceUpdateBeforeProcessing(){hasForceUpdate=false;}exportfunctioncheckHasForceUpdateAfterProcessing():boolean{returnhasForceUpdate;}exportfunctioncommitUpdateQueue<State>(finishedWork:Fiber,finishedQueue:UpdateQueue<State>,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 effectscommitUpdateEffects(finishedQueue.firstEffect,instance);finishedQueue.firstEffect=finishedQueue.lastEffect=null;commitUpdateEffects(finishedQueue.firstCapturedEffect,instance);finishedQueue.firstCapturedEffect=finishedQueue.lastCapturedEffect=null;}functioncommitUpdateEffects<State>(effect:Update<State>|null,instance:any,):void{while(effect!==null){constcallback=effect.callback;if(callback!==null){effect.callback=null;callCallback(callback,instance);}effect=effect.nextEffect;}}

至此,结束。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/15 11:29:58

Graph Unlearning---论文总结

一、研究背景 1、隐私法规与被遗忘权 近年来&#xff0c;随着《通用数据保护条例》&#xff08;GDPR&#xff09;、《加州消费者隐私法案》&#xff08;CCPA&#xff09;等法律法规的颁布&#xff0c;数据隐私保护成为了全球关注的焦点。其中最重要且最具争议的条款之一是 “…

作者头像 李华
网站建设 2025/12/15 11:29:31

Aave V4:从割裂市场到模块化流动性

撰文&#xff1a;Tia&#xff0c;Techub News 在 DeFi 借贷领域&#xff0c;Aave 一直是创新与行业标准的风向标。随着用户规模和资产种类的增长&#xff0c;Aave V3 逐渐暴露出流动性割裂、风险管理和清算机制相对粗糙的问题。为应对这些挑战&#xff0c;Aave V4 进行了系统性…

作者头像 李华
网站建设 2025/12/15 11:28:38

Kali_2025年最新版下载安装最全流程功能介绍(内附安装教程)

收藏必备&#xff01;零基础也能学会的Kali Linux安装与使用指南&#xff0c;网络安全学习首选系统 文章主要介绍了Kali Linux这一基于Debian的安全专用操作系统&#xff0c;包含其特点(开源免费、支持无线注入、高度可定制等)、适用人群(渗透测试者、安全研究员等)以及安装步…

作者头像 李华
网站建设 2025/12/15 11:28:22

详谈:解释器模式(三)

我们接上文来继续讲&#xff1a;计算符怎么处理呢&#xff1f;计算符左右两边可能是单个数字&#xff0c;也可能是另一个计算公式。但无论是数字还是公式&#xff0c;两者都有一个共同点&#xff0c;那就是他们都会返回一个整数&#xff1a;数字返回其本身&#xff0c;公式返回…

作者头像 李华
网站建设 2025/12/15 11:27:35

BooleanOperationPolyDataFilter 布尔运算的演示

一&#xff1a;主要的知识点 1、说明 本文只是教程内容的一小段&#xff0c;因博客字数限制&#xff0c;故进行拆分。主教程链接&#xff1a;vtk教程——逐行解析官网所有Python示例-CSDN博客 2、知识点纪要 本段代码主要涉及的有①vtkTriangleFilter三角面化&#xff0c;②…

作者头像 李华