React更新队列剖析

336 阅读22分钟

主要讲一下React更新队列

下列源码展示了展示了 React 中创建更新对象的过程。:

createUpdate(expirationTime, suspenseConfig):

  • createUpdate 函数是用于创建更新对象的工厂函数。
  • expirationTime 参数表示更新的过期时间,与任务的优先级相关联。这个时间决定了 React 何时处理这个更新。
  • suspenseConfig 参数用于表示更新的 suspense 配置,用于异步渲染时的暂停和恢复操作。

createUpdate函数是用于创建更新对象的工厂函数。 在React中,所有的状态更新都被表示为更新对象,这个函数负责创建这些对象。更新对象包含了表示状态变化的信息,例如要更新的内容,更新的类型等等。

expirationTime参数表示更新的过期时间,与任务的优先级相关联。 React中的更新操作是异步执行的,并且有不同的优先级。expirationTime参数指定了这个更新对象的过期时间,决定了React何时处理这个更新。具有更高优先级的更新会比低优先级的更新更早被处理。

suspenseConfig参数用于表示更新的suspense配置。 这个参数用于异步渲染时的暂停和恢复操作。Suspense是React的一个特性,它允许组件在异步加载数据时暂停渲染,并在数据加载完成后恢复渲染。suspenseConfig参数用于配置这种暂停和恢复的行为,以确保异步操作的正确执行。

综合起来,createUpdate函数允许你创建一个包含了状态更新信息的对象,并且可以通过expirationTime参数指定更新的优先级,以及通过suspenseConfig参数配置异步操作的暂停和恢复行为。这样的灵活性使得React能够高效地处理不同优先级和异步操作的状态更新。

update 对象包含以下属性:

  • expirationTime: 更新的过期时间,决定了更新何时被处理。
  • suspenseConfig: 更新的 suspense 配置,用于异步渲染时的控制。
  • tag: 标识更新的类型,可以是 UpdateState、ReplaceState、ForceUpdate 等,表示更新的种类。
  • payload: 更新的具体内容,例如新的 state。
  • callback: 更新完成后的回调函数。
  • next: 指向下一个更新对象,形成更新队列。
  • nextEffect: 指向下一个副作用对象,用于记录组件的生命周期方法等副作用。

对其中部分元素解释一下:

callback: 更新完成后的回调函数。当一个更新被处理并且组件重新渲染后,可以通过这个回调函数来执行一些额外的操作。例如,可以在组件更新完成后执行一些特定的逻辑或者触发其他函数。

next: 指向下一个更新对象。React将所有的更新对象按照顺序链接在一起,形成一个更新队列。next属性指向队列中的下一个更新对象,通过这种方式,React可以按照顺序依次处理更新。

nextEffect: 指向下一个副作用对象。在React的调度过程中,除了更新组件状态外,还可能伴随着一些副作用操作,例如调用组件的生命周期方法、处理Ref等。这些副作用被封装成副作用对象,并且按照顺序链接在一起,形成一个副作用链表。nextEffect属性指向链表中的下一个副作用对象,确保副作用的执行顺序。

在开发环境中,这个函数还会设置 update.priority,根据当前任务队列的执行情况来确定更新的优先级。

总体来说,这个函数的作用是创建一个描述更新操作的对象,其中包含了更新的类型、过期时间、具体内容等信息,这个对象会在 React 的调度过程中被使用,确保组件的状态更新被以正确的优先级和时间点进行处理。

创建一个用于描述更新操作的对象。在React中,当组件的状态需要被更新时,不会立即进行更新操作,而是将更新操作描述成一个更新对象。这个对象包含了更新的类型(例如,更新组件的状态、强制重新渲染等)、过期时间(表示这个更新的优先级,不同优先级的更新会有不同的处理顺序)以及具体的更新内容。

这个更新对象在React的内部调度过程中被使用。React会根据这些更新对象的优先级和时间点,合理地安排组件的状态更新。通过这种方式,React能够实现高效的异步更新,确保在适当的时机和以正确的优先级来处理组件的状态变化。

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdate(expirationTime, suspenseConfig) {
var update = {
// 过期时间与任务优先级相关联
expirationTime: expirationTime,
suspenseConfig: suspenseConfig,
// tag用于标识更新的类型如UpdateState,ReplaceState,ForceUpdate等
tag: UpdateState,
// 更新内容
payload: null,
// 更新完成后的回调
callback: null,
// 下一个更新(任务)
next: null,
// 下一个副作用
nextEffect: null
};
{
// 优先级会根据任务体系中当前任务队列的执行情况而定
update.priority = getCurrentPriorityLevel();
}
return update;
}

React对更新队列的定义如下

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdateQueue(baseState) {
var queue = {
// 当前的state
baseState: baseState,
// 队列中第一个更新
firstUpdate: null,
// 队列中的最后一个更新
lastUpdate: null,
// 队列中第一个捕获类型的update
firstCapturedUpdate: null,
// 队列中第一个捕获类型的update
lastCapturedUpdate: null,
// 第一个副作用
firstEffect: null,
// 最后一个副作用
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null
};
return queue;
}

queue 对象包含了更新队列的各个属性:

  • baseState: 表示当前的组件状态,即初始状态。
  • firstUpdate: 指向队列中的第一个更新对象,用于记录更新队列中的第一个待处理的更新。
  • lastUpdate: 指向队列中的最后一个更新对象,用于记录更新队列中的最后一个待处理的更新。
  • firstCapturedUpdate 和 lastCapturedUpdate: 指向队列中的第一个和最后一个捕获类型的更新对象,用于记录在错误边界(Error Boundary)内部的更新。
  • firstEffect 和 lastEffect: 指向副作用链表(Effect List)中的第一个和最后一个副作用对象,用于记录组件更新时需要执行的副作用(生命周期方法、hooks 等)。
  • firstCapturedEffect 和 lastCapturedEffect: 与 firstEffect 和 lastEffect 类似,但用于捕获类型的副作用链表。

当使用 setState(...) 时, React 会创建一个更新(update)对象,然后通过调用 enqueueUpdate 函数将其加入到更新队列(updateQueue)

在React组件中使用setState(...)时,React会内部创建一个更新对象(update object)。这个更新对象包含了关于要对组件状态进行的更新的所有信息,比如要更新的具体内容以及更新的优先级等。然后,React会调用enqueueUpdate函数将这个更新对象添加到组件的更新队列中(update queue)。

更新队列是一个数据结构,它用于存储所有等待处理的更新对象。这个队列按照一定的优先级顺序排列,React会根据更新对象的优先级,以及其他调度算法,来确定何时以及如何处理这些更新。这种异步的更新机制使得React能够高效地处理状态的变化,而不会阻塞用户界面的响应性。

所以,当你调用setState(...)时,实际上是在告诉React:“我有一个更新,它描述了组件状态的变化,把它加入到更新队列中,我稍后会处理它。”这样,React就能够在适当的时机,以合适的顺序,安排这些更新的处理,确保应用程序的正确性和性能。

function enqueueUpdate(fiber, update) {
// 每个Fiber结点都有自己的updateQueue,其初始值为null,一般只有ClassComponent类型的结点updateQueue才会被赋值
// fiber.alternate指向的是该结点在workInProgress树上面对应的结点
var alternate = fiber.alternate;
var queue1 = void 0;
var queue2 = void 0;
if (alternate === null) {
// 如果fiber.alternate不存在
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// 如果fiber.alternate存在,也就是说存在current树上的结点和workInProgress树上的结点都存在
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// 如果两个结点上面均没有updateQueue,则为它们分别创建queue
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);
} else {
// 如果只有其中一个存在updateQueue,则将另一个结点的updateQueue克隆到该结点
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// 如果只有其中一个存在updateQueue,则将另一个结点的updateQueue克隆到该结点
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// 如果两个结点均有updateQueue,则不需要处理
}
}
}
if (queue2 === null || queue1 === queue2) {
// 经过上面的处理后,只有一个queue1或者queue1 == queue2的话,就将更新对象update加入到queue1
appendUpdateToQueue(queue1, update);
} else {
// 经过上面的处理后,如果两个queue均存在
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// 只要有一个queue不为null,就需要将将update加入到queue中
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// 如果两个都不是空队列,由于两个结构共享,所以只在queue1加入update
appendUpdateToQueue(queue1, update);
// 仍然需要在queue2中,将lastUpdate指向update
queue2.lastUpdate = update;
}
}
...
}

首先,判断当前 Fiber 节点及其备份节点(如果存在)是否有更新队列。

如果 fiber.alternate 不存在,表示这是第一次渲染该组件。

如果当前 Fiber 节点的 fiber.updateQueue 为 null,则将其初始化为一个新的更新队列(使用 createUpdateQueue 函数)。

  • 如果 fiber.alternate 存在,表示当前 Fiber 节点和备份节点在两次渲染中都有存在。
  • 如果当前 Fiber 节点的 fiber.updateQueue 为 null,则:
  • 如果备份节点的 fiber.updateQueue 为 null,则同时为当前节点和备份节点创建新的更新队列。
  • 如果备份节点的 fiber.updateQueue 不为 null,则将备份节点的更新队列克隆到当前节点。
  • 判断两个队列是否可以共享。

如果两个队列都为 null 或者两个队列相同,说明它们可以共享更新对象。

将待处理的更新对象 update 添加到队列中(使用 appendUpdateToQueue 函数)。

  • 如果两个队列都存在,并且它们不相同。
  • 如果两个队列中有任意一个不为空(即至少有一个队列中有更新对象),将更新对象添加到两个队列中。
  • 如果两个队列的 lastUpdate 属性有任意一个为空,将更新对象添加到两个队列中。
  • 如果两个队列的 lastUpdate 属性都不为空,只将更新对象添加到当前节点的队列中,并将备份节点的 lastUpdate 指向这个更新对象。

这段代码的作用是确保更新对象被正确地添加到当前 Fiber 节点的更新队列中,以便在后续的更新过程中正确地处理状态的变化。这种设计允许 React 在更新时高效地管理组件的状态,并在需要时将更新同步到 DOM 中。

在 React 中,某些情况下一个组件可能同时在两棵 Fiber 树中存在,例如在更新和并发模式下。

如果组件同时存在于当前渲染树和备份(alternate)渲染树中,React 会确保这两棵树的更新队列保持同步。这就需要将更新对象添加到这两个队列中,确保在不同的情况下都能正确地处理状态更新。

在React中,存在一种并发模式,即同时处理多个Fiber树的更新。在这种情况下,同一个组件可能会同时存在于当前渲染树(current tree)和备份渲染树(alternate tree)中。备份渲染树通常是用于实现一种并发渲染的机制。

React必须确保这两棵树的状态保持同步,这就涉及到了更新队列的处理。当一个组件在两棵树中都存在时,React会将状态更新的操作添加到这两个队列中。这意味着,如果一个组件在当前渲染树中触发了状态更新,这个更新操作会被添加到当前渲染树的更新队列中。同时,React还会将相同的更新操作添加到备份渲染树的更新队列中。

这样做的目的是为了保持两个渲染树的状态同步。因为在并发模式下,React可能会选择其中一棵树进行渲染,而另一棵树则被用于预处理或者备份。通过在两个队列中都添加相同的更新操作,React能够确保无论哪棵树被选择,组件的状态都会得到正确的更新。这种机制保证了React在并发模式下的一致性和可靠性。

在 React 的更新机制中,lastUpdate 属性用于指向最后一个添加到队列的更新对象。通过将 lastUpdate 指向新的更新对象,React 可以追踪每个组件的状态变化。

当一个组件在不同 Fiber 树中存在时,React 会确保这两个树的 lastUpdate 属性相同,这就需要在两个队列中都正确地处理 lastUpdate。

在React的更新机制中,每个组件的状态变化都被抽象成一个个更新对象,这些更新对象被添加到组件的更新队列中。队列中的lastUpdate属性指向队列中的最后一个更新对象,也就是最新的更新。通过lastUpdate,React能够追踪每个组件的状态变化。

当一个组件在不同的Fiber树中存在时,例如在并发模式下,React需要确保这两个Fiber树的状态保持同步。为了实现这个目的,React需要确保这两个树中的lastUpdate属性是相同的,即它们都指向各自队列中的最后一个更新对象。

当在一个队列中添加新的更新时,React会更新该队列的lastUpdate属性,使其指向新添加的更新对象。同时,React还会将相同的更新操作添加到另一个队列中,确保两个队列的lastUpdate保持一致。这样,无论哪个Fiber树被选择进行渲染,组件的状态都会得到正确的更新,保持两个树的状态同步。这种机制保证了React在不同Fiber树之间的状态一致性,是React在并发模式下保持数据一致性的关键机制之一。

function appendUpdateToQueue(queue, update) {
if (queue.lastUpdate === null) {
// 如果队列为空,则第一个更新和最后一个更新都赋值当前更新
queue.firstUpdate = queue.lastUpdate = update;
} else {
// 如果队列不为空,将update加入到队列的末尾
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}

React 内部用于管理组件状态更新队列的一部分。当组件的状态需要更新时,React 会将这些更新操作存储在一个更新队列中。这个函数负责将一个新的更新对象(update)添加到队列(queue)的末尾。

判断队列是否为空:

如果队列为空(queue.lastUpdate === null),表示这是队列的第一个更新对象。

在这种情况下,将队列的 firstUpdate 和 lastUpdate 指向当前这个新的更新对象,因为它既是第一个也是最后一个更新。

存储 React 组件中的状态更新的队列。React 使用队列来管理组件的状态更新,保证更新操作的顺序和一致性。

在这个数据结构中,每一个更新操作都是一个对象。当第一个更新对象被加入队列时,这个对象会成为队列的第一个(firstUpdate)也是最后一个(lastUpdate)更新对象,因为队列中只有它一个对象。在这种情况下,firstUpdate 和 lastUpdate 都指向这个对象。

当队列不为空时,也就是队列中已经有了其他的更新对象,新的更新对象需要加入到队列的末尾。为了做到这一点,React 将当前队列的 lastUpdate.next 属性指向新的更新对象。这个操作的结果是,之前队列中的最后一个更新对象(lastUpdate)的 next 属性现在指向了当前这个新的更新对象,表示新的更新对象被加入到了队列的末尾。这样,整个队列就保持了正确的顺序,最后一个更新对象的 next 属性总是指向新加入的更新对象,确保了更新操作的顺序和一致性。

队列不为空时:

如果队列不为空,说明队列中已经有了其他的更新对象。

将当前队列的 lastUpdate.next 属性指向新的更新对象。这样,之前队列中的最后一个更新对象(lastUpdate)的 next 属性就指向了当前这个新的更新对象。

更新队列的 lastUpdate 指针移动到新的更新对象上,表示这个新的更新对象现在是队列中的最后一个。

在React中,状态更新操作通过更新队列进行管理。这个队列中的每个元素都是一个包含了要更新的数据和相关信息的对象。当一个新的状态更新操作被触发时,一个新的更新对象会被创建。在这个描述中,"更新队列的 lastUpdate 指针移动到新的更新对象上" 意味着,React将这个新的更新对象放入了更新队列的末尾,并将队列中的 lastUpdate 指针指向了这个新的更新对象。

这个动作的目的是保持队列中更新操作的顺序。通过将新的更新对象放到队列末尾,React确保了状态更新操作的执行顺序。当组件重新渲染时,React会按照更新队列中操作的顺序来应用这些更新,以确保状态的变化按照预期的方式发生。因此,lastUpdate 指针的移动到新的更新对象上,表示新的状态更新已经被加入到队列中,并且会在适当的时候被处理。

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime) {
...
// 从队列中取出第一个更新
var update = queue.firstUpdate;
var resultState = newBaseState;
// 遍历更新队列,处理更新
while (update !== null) {
...
// 如果第一个更新不为空,紧接着要遍历更新队列
// getStateFromUpdate函数用于合并更新,合并方式见下面函数实现
resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);
...
update = update.next;
}
...
// 设置当前fiber结点的memoizedState
workInProgress.memoizedState = resultState;
...
}
// 获取下一个更新对象并与现有state对象合并
function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
switch (update.tag) {
case UpdateState:
{
var _payload2 = update.payload;
var partialState = void 0;
if (typeof _payload2 === 'function') {
// setState传入的参数_payload2类型是function
...
partialState = _payload2.call(instance, prevState, nextProps);
...
} else {
// setState传入的参数_payload2类型是object
partialState = _payload2;
}
// 合并当前state和上一个state.
return _assign({}, prevState, partialState);
}
}
}

这段源码是 React 内部用于处理组件状态更新队列的一部分。当组件的状态需要更新时,React 会将更新操作存储在一个更新队列中。

processUpdateQueue 函数:

这个函数被调用时,它接收当前工作中的 Fiber 节点(workInProgress)、更新队列(queue)、组件的属性(props)、组件实例(instance)以及当前渲染的过期时间(renderExpirationTime)作为参数。

函数首先从更新队列中取出第一个更新对象(update)。

然后,它遍历更新队列中的所有更新对象,依次处理这些更新对象。在处理过程中,会根据更新的类型调用 getStateFromUpdate 函数,将当前状态(prevState)与更新对象中的数据进行合并,得到新的状态。

最终,resultState 变量保存了所有更新被合并后的状态。这个状态将被设置为当前 Fiber 节点的 memoizedState 属性,表示这个组件的最新状态。

resultState 变量保存了所有更新被合并后的状态: 在更新队列中,可能存在多个更新操作,这些操作可能是由 setState 函数触发的,每个更新操作可能修改了组件的一部分状态。在处理完所有的更新操作后,resultState 包含了这些更新操作被合并后的最终状态。

memoizedState 属性表示这个组件的最新状态: 在 React 的内部实现中,每个组件对应一个 Fiber 节点。memoizedState 是 Fiber 节点的一个属性,用于存储组件的状态。在更新队列中的所有更新操作都被处理和合并后,最终得到的 resultState 就代表了组件的最新状态。 这个 resultState 被设置到当前的 Fiber 节点的 memoizedState 属性上,意味着组件的状态已经被成功更新。

总的来说,这段描述表明在 React 的更新过程中,通过处理和合并多个更新操作,得到了组件的最终状态,并将这个状态存储在了当前 Fiber 节点的 memoizedState 属性中,以便在后续的渲染过程中使用。这种机制确保了组件状态的一致性和正确性,使得 React 能够在组件状态发生变化时正确地更新用户界面。

getStateFromUpdate 函数:

这个函数用于处理单个更新对象。它接收当前工作中的 Fiber 节点(workInProgress)、更新队列(queue)、单个更新对象(update)、上一个状态(prevState)、组件的属性(nextProps)以及组件实例(instance)作为参数。

在函数中,根据更新对象的类型(这里只处理了 UpdateState 类型),它从更新对象中获取 payload 属性,这个属性保存了要更新的数据。

如果 payload 是一个函数,说明开发者使用了函数形式的 setState,它会执行这个函数,并将上一个状态和当前属性传入,得到部分状态(partialState)。当payload参数是一个函数时,说明开发者使用了函数形式的setState。这个函数会被执行,并且会接收两个参数:上一个状态(prevState)和当前的属性(props)。函数内部可以根据这两个参数计算出部分状态(partialState),表示组件新的状态。React会将这个部分状态与当前的状态合并,得到新的状态。

如果 payload 是一个普通对象,说明开发者使用了对象形式的 setState,它会直接将这个对象作为部分状态(partialState)。当payload参数是一个普通对象时,说明开发者使用了对象形式的setState。这个对象直接表示了组件新的状态(partialState)。React会将这个对象作为部分状态,与当前的状态进行合并,得到新的状态。

最后,函数将上一个状态(prevState)和部分状态(partialState)合并,得到新的状态,然后返回。

当开发者使用 setState 更新组件的状态时,可以传递两种不同类型的参数:对象形式的参数和函数形式的参数。

对象形式的参数(payload 是一个普通对象): 如果开发者使用了对象形式的 setState,就像这样:this.setState({ key: value })。React 会直接将这个对象作为部分状态,也就是 partialState。这个部分状态指的是要更新的状态的一部分,而不是整个新的状态。当传递一个普通对象时,该对象表示组件的新状态的一部分(partialState)。React会将这个对象与当前状态进行合并,生成新的状态

函数形式的参数(payload 是一个函数): 如果开发者使用了函数形式的 setState,就像这样:this.setState((prevState, props) => ({ key: value }))。这个函数接收两个参数:prevState 表示当前组件的状态,props 表示当前的属性。这个函数的返回值就是部分状态(partialState)。

在处理 setState 的更新时,React 会将上一个状态(prevState)和部分状态(partialState)合并,得到新的状态。这个合并操作通常是将两个对象的属性合并到一个新的对象中。例如:

在处理setState()函数的更新时,React会将上一个状态(prevState)和部分状态(partialState)进行合并,以得到新的状态。这个合并操作通常是将两个对象的属性合并到一个新的对象中。

当你在React组件中调用setState()函数时,你可以传递一个对象,这个对象表示你想要更新的部分状态。React会将这个对象的属性与当前状态(prevState)合并,以创建新的状态。这种合并操作确保了组件的状态保持了一致性和可预测性。合并操作确保了你可以选择性地更新组件的状态,而不需要手动处理整个状态对象。React会负责处理状态的合并,使得状态更新变得更加方便和可控。

const newState = { ...prevState, ...partialState };

最终,这个新的状态会被设置为组件的最新状态,确保了组件状态的正确性和一致性。这种机制允许开发者根据组件的当前状态和属性来计算新的状态,使得组件能够正确地响应状态的变化。

总结来说,这些函数是 React 内部用于处理组件状态更新的关键部分。它们负责从更新队列中取出更新对象,根据不同类型的更新将状态进行合并,最终得到最新的组件状态,确保组件在状态更新时能够正确地进行渲染和重渲染。