跳到主要内容

认识

2023年12月19日
柏拉文
越努力,越幸运

一、认识


React.js 中的 UpdateQueueReact 更新组件状态的内部机制。它管理着组件状态 state 和属性 props 变化的队列。 当一个组件的 state 或者 props 发生变化时, React 将这些变化作为更新 Update 加入到 UpdateQueue 队列, 后续进行批处理和异步更新。Update 更新具有不同的优先级, 来自用户交互的更新会被赋予更高的优先级, UpdateQueue 任务队列确保高优先级更新任务首先被处理。在处理更新任务时, 如果有一个更高优的任务加入到 UpdateQueue 队列, 会中断当前的更新, 转而处理更高有的更新,后续再恢复中断的更新。如果低优先更新任务一直得不到执行, 那么之后会将这个任务优先级提高, 防止饿死。

二、细节


2.1 Update

const update: Update<mixed> = {
lane,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
  • lane: 当前 update 更新优先级

  • payload: 更新挂载的数据, useState dispatch 中的 update.payload 挂载的是用户传入的数据, createRoot(container).render() 中的 update.payload 挂载的是 JSX 节点。

  • callback: 更新的回调函数

  • next: 与其他 Update 连接形成链表

2.2 UpdateQueue

const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
callbacks: null,
};

fiber.updateQueue = queue;
  • baseState: 初始值为 fiber.memoizedState。更新后为上次更新时第一个优先级任务的前一个 Update 计算后的结果, 如果没有低优先级任务为所有任务计算后的最终结果。同时也是本次更新参与计算的初始 state

  • firstBaseUpdate: 上次更新时, 由于某些 Update 优先级较低, 计算时会被跳过, 这些 Update 会组成一个链表, firstBaseUpdate 表示该链表的头节点

  • lastBaseUpdate: 上次更新时, 由于某些 Update 优先级较低, 计算时会被跳过, 这些 Update 会组成一个链表, firstBaseUpdate 表示该链表的尾节点

  • shared.pending: 触发更新时,产生的 Update 会保存在 shared.pending 中形成单向环状链表。当由 Update 计算 state 时这个环会被剪开并连接在 lastBaseUpdate 后面

  • effects: 数组。保存 update.callback !== nullUpdate

2.3 UpdateQueue 链表

UpdateQueue 主要存在两个链表:

本次更新要执行任务的链表: 本次更新时新增的任务,会放到 shared.pending 中,形成一个单向环形链表。 计算 Update 时, 会将其拆成单向链表,拼接 lastBaseUpdate 后面

上次更新时优先级不够被被跳过的任务链表: 每次调度时都会判断当前任务是否有足够的优先级来执行,若优先级不够,则重新存储到链表中,用于下次渲染时重新调度,而 firstBaseUpdatelastBaseUpdate 就是低优先级任务的头指针尾指针; 若 firstBaseUpdatenull,说明这可能是第一次渲染,或者上次所有的任务的优先级都足够,全部执行了

三、工作


ReactDOM.render()createRoot().render()this.setStateuseState dispatchuseReducer dispatchthis.forceUpdate 共用同一套状态更新机制。

3.1 初始化 UpdateQueue

初始化 UpdateQueue: 初始化一个 UpdateQueue,并将 updateQueue 赋值给 fiber.updateQueueupdateQueue 队列是 fiber 更新时要执行的内容。代码如下:

export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes,
},
effects: null,
};
fiber.updateQueue = queue;
}

3.2 创建 Update 数据

创建 update: update 保存更新状态的相关内容。代码如下:

export function createUpdate(lane: Lane): Update<mixed> {
const update: Update<mixed> = {
lane,

tag: UpdateState,
payload: null,
callback: null,

next: null,
};
return update;
}

3.3 进入 UpdateQueue 队列

进入 UpdateQueue 队列: 向链表 fiber.updateQueue.shared.pending 中添加 Update 节点, 形成一个单向环形链表。之所以设计为单向环形链表, 原因如下:

  1. 做成环形链表可以只需要利用一个指针,便能找到头节点与尾节点

  2. 更加方便地找到最后一个 Update 对象,同时插入新的 Update 对象也非常方便

  3. 如果使用普通的线性链表,就需要同时记录第一个和最后一个节点的位置,维护成本相对较高

代码如下:

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>, lane: Lane) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// 只有在fiber已经被卸载了才会出现
// Only occurs if the fiber has been unmounted.
return;
}

const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

if (isInterleavedUpdate(fiber, lane)) {
// 省略
} else {
const pending = sharedQueue.pending;

if (pending === null) {
// This is the first update. Create a circular list.
/**
* 当pending为null时,说明链表中还没有节点,update为第1个节点,
* 自己指向自己,最后面的pending指向到update
*/
update.next = update;
} else {
/**
* 当已经存在节点时,pending指向的是最后一个节点,pending.next是指向的第一个节点,
* update.next = pending.next:即update的next指向到了第一个节点,
* pending.next = update:即最后一个节点pending的next指针指向到了update节点,
* 这样update就进入到链表中了,此时update是链表的最后一个节点了,
* 然后下面的 sharedQueue.pending 再指向到租后一个 update 节点
*/
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}

3.4 计算 Update 状态

计算 Update 状态, 主要逻辑如下:

  1. 获取上次更新时因优先级较低而跳过组成的链表的头结点 firstBaseUpdate 与尾节点 lastBaseUpdate, 同时将 shared.pending 拆开拼接到尾节点 lastBaseUpdate 的后面

  2. 设置 newState 用于存储 Update 计算结果, 设置 newBaseState 用于存储第一个低优先级任务之前的 Update 计算结果 newState, 若不存在低优先级的任务,则 newBaseState 为执行完所有任务后得到的值 newState。设置 newLastBaseUpdate 用于存储低优先级的更新链表。

  3. 遍历 firstBaseUpdate 链表, 依次判断当前 Update 对应的任务优先级是否足够

  4. Update 优先级较低: 如果 newLastBaseUpdate 为空, 说明此时的 Update 是第一个低优先级任务, 更新 newBaseState 为上一次 Update 计算结果 newState。 如果 newLastBaseUpdate 不为空, 将低优先级任务加入到 newLastBaseUpdate 队列即可。

  5. Update 优先级足够: 如果存储低优先级任务链表的 newLastBaseUpdate 不为空, 为了保证操作的完整性, 将 Update 加入到 newLastBaseUpdate 后面。执行 Update, 获取最新计算结果 newState

  6. 最后, 将执行所有操作后得到的 newState 赋值给 fiber.memoizedState。将 newBaseState 赋值给 updateQueue.baseState, 作为下次的初始值。将 newLastBaseUpdate 赋值给 updateQueue.lastBaseUpdate, 用于下次更新。

代码如下所示

export function processUpdateQueue<State>(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes): void {
// This is always non-null on a ClassComponent or HostRoot
// 在 HostRoot和 ClassComponent的fiber节点中,updateQueue不可能为null
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);

hasForceUpdate = false;

/**
* queue.shared.pending本身是一个环形链表,即使有一个节点,也会形成环形链表,
* 而且 queue.shared.pending 指向的是环形链表的最后一个节点,这里将其断开形成单向链表
* 单向链表的头指针存放到 firstBaseUpdate 中,最后一个节点则存放到 lastBaseUpdate 中
*/
let firstBaseUpdate = queue.firstBaseUpdate; // 更新链表的开始节点
let lastBaseUpdate = queue.lastBaseUpdate; // 更新链表的最后的那个节点

// 检测是否存在将要进行的更新,若存在,则将其拼接到 lastBaseUpdate 的后面,并清空刚才的链表
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
queue.shared.pending = null;

/**
* 若pending queue 是一个环形链表,则将第一个和最后一个节点断开,
* 环形链表默认指向的是最后一个节点,因此 pendingQueue 指向的就是最后一个节点,
* pendingQueue.next(lastPendingUpdate.next)就是第一个节点了
*/
const lastPendingUpdate = pendingQueue; // 环形链表的最后一个节点
const firstPendingUpdate = lastPendingUpdate.next; // 环形链表的第一个节点
lastPendingUpdate.next = null; // 最后一个节点与第一个节点断开

/**
* 将 pendingQueue 拼接到 更新链表 queue.firstBaseUpdate 的后面
* 1. 更新链表的最后那个节点为空,说明当前更新链表为空,把要更新的首节点 firstPendingUpdate 给到 firstBaseUpdate即可;
* 2. 若更新链表的尾节点不为空,则将要更新的首节点 firstPendingUpdate 拼接到 lastBaseUpdate 的后面;
* 3. 拼接完毕后,lastBaseUpdate 指向到新的更新链表最后的那个节点;
*/
// Append pending updates to base queue
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;

/**
* 若workInProgress对应的在current的那个fiber节点,其更新队列的最后那个节点与当前的最后那个节点不一样,
* 则我们将上面「将要更新」的链表的头指针和尾指针给到current节点的更新队列中,
* 拼接方式与上面的一样
*/
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;

// 若current更新链表的最后那个节点与当前将要更新的链表的最后那个节点不一样
// 则,把将要更新的链表也拼接到current中
if (currentLastBaseUpdate !== lastBaseUpdate) {
if (currentLastBaseUpdate === null) {
currentQueue.firstBaseUpdate = firstPendingUpdate;
} else {
currentLastBaseUpdate.next = firstPendingUpdate;
}
currentQueue.lastBaseUpdate = lastPendingUpdate;
}
}
}

/**
* 进行到这里,render()初始更新时,放在 queue.shared.pending 中的update节点(里面存放着element结构),
* 就已经放到 queue.firstBaseUpdate 里了,
* 因此 firstBaseUpdate 里肯定存放了一个 update 节点,一定不为空,进入到 if 的逻辑中
*/
// These values may change as we process the queue.
if (firstBaseUpdate !== null) {
// Iterate through the list of updates to compute the result.
// 迭代更新列表以计算结果

/**
* newState 先拿到上次的数据,然后执行 firstBaseUpdate 链表中所有的 update,
* 再存储每轮的结果,最后将其给到 workInProgress.memoizedState
* 默认值:
* {
* cache: {controller: AbortController, data: Map(0), refCount: 1}
* element: null
* isDehydrated: false
* pendingSuspenseBoundaries: null
* transitions: null
* }
*/
let newState = queue.baseState;
// TODO: Don't need to accumulate this. Instead, we can remove renderLanes
// from the original lanes.
let newLanes = NoLanes;

/**
* 下次渲染时的初始值
* 1. 若存在低优先级的任务,则该 newBaseState 为第一个低优先级任务之前计算后的值;
* 2. 若不存在低优先级的任务,则 newBaseState 为执行完所有任务后得到的值;
*/
let newBaseState = null;

/**
* 下面的两个指针用来存放低优先级的更新链表,
* 即 firstBaseUpdate 链表中,可能会存在一些优先级不够的update,
* 若存在低优先级的update,则将其拼接到 newFirstBaseUpdate 里,
* 同时,既然存在低优先级的任务,为了保证整个更新的完整性,也会将已经执行update后的结果,也放到这个新链表中,
* 这里存在一个问题,若低优先级任务是中间才出现的,怎么办呢?
* 解决方案:将执行到当前update前的state设置为新链表的初始值:newBaseState = newState;
*/
let newFirstBaseUpdate = null; // 新的更新链表的头指针
let newLastBaseUpdate = null; // 新的更新链表的尾指针

let update = firstBaseUpdate; // 从第1个节点开始执行
do {
const updateLane = update.lane;
const updateEventTime = update.eventTime;

/**
* 判断 updateLane 是否是 renderLanes 的子集,
* if 这里有个取反的符号,导致理解起来可能有点困难,实际上:
* 1. 若 update 的 lane (又名 updateLane) 是 renderLanes 的子集,则执行该update;
* 2. 若不是其子集,则将其放到心的队列中,等待下次的执行;
*/
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
/**
* 若当前 update 的操作的优先级不够。跳过此更新。
* 将该update放到新的队列中,为了保证链式操作的连续性,下面else逻辑中已经可以执行的update,也放到这个队列中,
* 这里还有一个问题,从第一个低优先级的任务到最后都已经存储起来了,那新的初始状态是什么呢?
* 新的初始状态就是当前跳过的update节点时的那个状态。新的初始状态,只有在第一个跳过任务时才需要设置。
* 例如我们初始状态是0,有10个update的操作,第0个update的操作是+0,第1个update的操作是+1,第2个update的操作是+2,依次类推;
* 若第4个update是一个低优先级的操作,其他的都是正常的优先级。
* 那么将第4个update放到新的链表进行存储时,此时要存储的初始值就是执行当前节点前的值,是6(state+0+1+2+3)
* 后续的update即使当前已经执行过了,也是要放到新的链表中的,否则更新就会乱掉。
* 下次渲染时,就是以初始state为6,+4的那个update开始,重新判断优先级
*/
const clone: Update<State> = {
eventTime: updateEventTime,
lane: updateLane,

tag: update.tag,
payload: update.payload,
callback: update.callback,

next: null,
};
// 拼接低优先级的任务
if (newLastBaseUpdate === null) {
// 还没有节点,这clone就是头结点
// 并将此时的 newState 放到新的 newBaseState中
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.
// 此更新具有足够的优先级
// 初始render()时会走这里

if (newLastBaseUpdate !== null) {
/**
* 若存储低优先级的更新链表不为空,则为了操作的完整性,即使当前update会执行,
* 也将当前的update节点也拼接到后面,
* 但初始render()渲染时,newLastBaseUpdate为空,走不到 if 这里
*/
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.
/**
* 翻译:这次update将要被提交更新,因此后续我们不希望取消这个提交。
* 使用 NoLane 这个是可行的,因为0是任何掩码的子集,
* 所以上面 if 的检测`isSubsetOfLanes(renderLanes, updateLane)`,永远都会为真,
* 该update永远不会被作为低优先级进行跳过,每次都会执行
*/
lane: NoLane,

tag: update.tag,
payload: update.payload,
callback: update.callback,

next: null,
};
// 拼接到低优先级链表的后面
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}

// Process this update.
/**
* render()时 newState 的默认值:
* {
* cache: {controller: AbortController, data: Map(0), refCount: 1}
* element: null
* isDehydrated: false
* pendingSuspenseBoundaries: null
* transitions: null
* }
* 执行 getStateFromUpdate() 后,则会将 update 中的 element 给到 newState 中
*/
newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance);
const callback = update.callback;
if (
callback !== null &&
// If the update was already committed, we should not queue its
// callback again.
update.lane !== NoLane
) {
workInProgress.flags |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
}
update = update.next; // 初始render()时,只有一个update节点,next为null,直接break,跳出循环
if (update === null) {
/**
* 在上面将 queue.shared.pending 放到firstBaseUpdate时,
* queue.shared.pending就已经重置为null了
* @type {Update<State>|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) {
// 若没有任意的低优先级的任务呢,则将一串的update执行后的结果,就是新的 baseState,
// 若有低优先级的任务,则已经在上面设置过 newBaseState 了,就不能在这里设置了
newBaseState = newState;
}

queue.baseState = ((newBaseState: any): State); // 下次更新时,要使用的初始值
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;

/**
* 经过上面的操作,queue(即 workInProgress.updateQueue )为:
* baseState: { element: element结构, isDehydrated: false }
* effects: null,
* firstBaseUpdate: null,
* lastBaseUpdate: null,
* shared: { pending: null, interleaved: null, lanes: 0 }
*/

// workInProgress.updateQueue的数据结构: https://mat1.gtimg.com/qqcdn/tupload/1659687672451.png

// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
const lastInterleaved = queue.shared.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
newLanes = mergeLanes(newLanes, interleaved.lane);
interleaved = ((interleaved: any).next: Update<State>);
} while (interleaved !== lastInterleaved);
} else 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; // 存储本次最新的结果
}
}

参考资料


React18 源码解析之 processUpdateQueue 的执行