文章主要解释了 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&¤tlyProcessingQueue===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;}}至此,结束。