关于javascript:扒一扒React计算状态的原理

43次阅读

共计 12093 个字符,预计需要花费 31 分钟才能阅读完成。

点击进入 React 源码调试仓库。

概述

一旦用户的交互产生了更新,那么就会产生一个 update 对象去承载新的状态。多个 update 会连接成一个环装链表:updateQueue,挂载 fiber 上,
而后在该 fiber 的 beginWork 阶段会循环该 updateQueue,顺次解决其中的 update,这是解决更新的大抵过程,也就是计算组件新状态的实质。在 React 中,类组件与根组件应用一类 update 对象,函数组件则应用另一类 update 对象,然而都遵循一套相似的解决机制。暂且先以类组件的 update 对象为主进行解说。

相干概念

更新是如何产生的呢?在类组件中,能够通过调用 setState 产生一个更新:

this.setState({val: 6});

而 setState 实际上会调用 enqueueSetState,生成一个 update 对象,并调用enqueueUpdate 将它放入 updateQueue。

const classComponentUpdater = {enqueueSetState(inst, payload, callback) {
 ...
 // 根据事件优先级创立 update 的优先级
 const lane = requestUpdateLane(fiber, suspenseConfig);
 const update = createUpdate(eventTime, lane, suspenseConfig);
 update.payload = payload; enqueueUpdate(fiber, update);
 // 开始调度
 scheduleUpdateOnFiber(fiber, lane, eventTime);
 ... },};

假如 B 节点产生了更新,那么 B 节点的 updateQueue 最终会是是如下的状态:

         A 
        /
       /
      B ----- updateQueue.shared.pending = update————
     /                                       ^       |
    /                                        |_______|
   C -----> D
 

updateQueue.shared.pending 中存储着一个个的 update。
上面咱们解说以下 update 和 updateQueue 的构造。

update 的构造

update 对象作为更新的载体,必然要存储更新的信息

const update: Update<*> = {
 eventTime,
 lane,
 suspenseConfig,
 tag: UpdateState,
 payload: null,
 callback: null,
 next: null,
};
  • eventTime:update 的产生工夫,若该 update 始终因为优先级不够而得不到执行,那么它会超时,会被立即执行
  • lane:update 的优先级,即更新优先级
  • suspenseConfig:工作挂起相干
  • tag:示意更新是哪种类型(UpdateState,ReplaceState,ForceUpdate,CaptureUpdate)
  • payload:更新所携带的状态。

    • 类组件中:有两种可能,对象({}),和函数((prevState, nextProps):newState => {})
    • 根组件中:是 React.element,即 ReactDOM.render 的第一个参数
  • callback:可了解为 setState 的回调
  • next:指向下一个 update 的指针

updateQueue 的构造

在组件上有可能产生多个 update,所以对于 fiber 来说,须要一个链表来存储这些 update,这就是 updateQueue,它的构造如下:

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

咱们假如当初产生了一个更新,那么以解决这个更新的时刻为基准,来看一下这些字段的含意:

  • baseState:前一次更新计算得出的状态,它是第一个被跳过的 update 之前的那些 update 计算得出的 state。会以它为根底计算本次的 state
  • firstBaseUpdate:前一次更新时 updateQueue 中第一个被跳过的 update 对象
  • lastBaseUpdate:前一次更新中,updateQueue 中以第一个被跳过的 update 为终点始终到的最初一个 update 截取的队列中的最初一个 update。
  • shared.pending:存储着本次更新的 update 队列,是理论的 updateQueue。shared 的意思是 current 节点与 workInProgress 节点共享一条更新队列。
  • effects:数组。保留 update.callback !== null 的 Update

有几点须要解释一下:

  1. 对于产生多个 update 对象的场景,屡次调用 setState 即可
this.setState({val: 2});
this.setState({val: 6});

产生的 updateQueue 构造如下:

能够看出它是个单向的环装链表

 u1 ---> u2
 ^        |
 |________|
  1. 对于更新队列为什么是环状。

论断是:这是因为不便定位到链表的第一个元素。updateQueue 指向它的最初一个 update,updateQueue.next 指向它的第一个 update。

试想一下,若不应用环状链表,updateQueue 指向最初一个元素,须要遍历能力获取链表首部。即便将 updateQueue 指向第一个元素,那么新增 update 时依然要遍历到尾部能力将新增的接入链表。而环状链表,只需记住尾部,无需遍历操作就能够找到首部。了解概念是重中之重,上面再来看一下实现:

function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {const updateQueue = fiber.updateQueue; if (updateQueue === null) {return;} const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; // ppending 是真正的 updateQueue,存储 update
 const pending = sharedQueue.pending; if (pending === null) { // 若链表中没有元素,则创立单向环状链表,next 指向它本人
 update.next = update; } else { // 有元素,现有队列(pending)指向的是链表的尾部 update,// pending.next 就是头部 update,新 update 会放到现有队列的最初
 // 并首尾相连
 // 将新队列的尾部(新插入的 update)的 next 指向队列的首部,实现
 // 首位相连
 update.next = pending.next; // 现有队列的最初一个元素的 next 指向新来的 update,实现把新 update
 // 接到现有队列上
 pending.next = update; } // 现有队列的指针总是指向最初一个 update,能够通过最初一个寻找出整条
 // 链表
 sharedQueue.pending = update;}
  1. 对于 firstBaseUpdate 和 lastBaseUpdate,它们两个其实组成的也是一个链表:baseUpdate,以以后这次更新为基准,这个链表存储的是上次 updateQueue 中第一个被跳过的低优先级的 update,到队列中最初一个 update 之间的所有 update。对于 baseState,它是第一个被跳过的 update 之前的那些 update 计算的 state。

这两点略微不好了解,上面用例子来阐明:比方有如下的 updateQueue:

A1 -> B1 -> C2 -> D1 - E2

字母示意 update 携带的状态,数字示意 update 携带的优先级。Lanes 模型中,可了解为数越小,优先级越高,所以 1 > 2

第一次以 1 的渲染优先级解决队列,遇到 C2 时,它的优先级不为 1,跳过。那么直到这次解决完 updateQueue 时,此时的 baseUpdate 链表为

C2 -> D1 - E2

本次更新实现后,firstBaseUpdate 为 C2,lastBaseUpdate 为 E2,baseState 为ABD

用 firstBaseUpdate 和 lastBaseUpdate 记录下被跳过的 update 到最初一个 update 的所有 update,用 baseState 记录下被跳过的 update 之前那些 update 所计算出的状态。这样做的目标是保障最终 updateQueue 中所有优先级的 update 全副解决完时候的后果与预期后果保持一致。也就是说,只管 A1 -> B1 -> C2 -> D1 - E2 这个链表在第一次以优先级为 1 去计算的后果为 ABD(因为优先级为 2 的都被跳过了),但最终的后果肯定是 ABCDE,因为这是队列中的所有 update 对象被全副解决的后果,下边来具体分析 updateQueue 的解决机制。

更新的解决机制

解决更新分为三个阶段:筹备阶段、解决阶段、实现阶段。前两个阶段次要是解决 updateQueue,最初一个阶段来将新计算的 state 赋值到 fiber 上。

筹备阶段

整顿 updateQueue。因为优先级的起因,会使得低优先级更新被跳过期待下次执行,这个过程中,又有可能产生新的 update。所以当解决某次更新的时候,有可能会有两条 update 队列:上次遗留的和本次新增的 上次遗留的 就是从 firstBaseUpdate 到 lastBaseUpdate 之间的所有 update;本次新增的 就是新产生的那些的 update。

筹备阶段阶段次要是将两条队列合并起来,并且合并之后的队列不再是环状的,目标不便从头到尾遍历解决。另外,因为以上的操作都是解决的 workInProgress 节点的 updateQueue,所以还须要在 current 节点也操作一遍,放弃同步,目标在渲染被高优先级的工作打断后,再次以 current 节点为原型新建 workInProgress 节点时,不会失落之前尚未解决的 update。

解决阶段

循环解决上一步整顿好的更新队列。这里有两个重点:

  • 本次更新是否解决 update 取决于它的优先级(update.lane)和渲染优先级(renderLanes)。
  • 本次更新的计算结果基于 baseState。

优先级有余

优先级有余的 update 会被跳过,它除了跳过之外,还做了三件事:

  1. 将被跳过的 update 放到 firstBaseUpdate 和 lastBaseUpdate 组成的链表中,(就是 baseUpdate),期待下次解决低优先级更新的时候再解决。
  2. 记录 baseState,此时的 baseState 为该低优先级 update 之前所有已被解决的更新的后果,并且只在第一次跳过期记录,因为低优先级工作重做时,要从第一个被跳过的更新开始解决。
  3. 将被跳过的 update 的优先级记录下来,更新过程行将完结后放到 workInProgress.lanes 中,这点是调度得以再次发动,进而重做低优先级工作的要害。

对于第二点,ReactUpdateQueue.js 文件头部的正文做了解释,为了便于了解,我再解释一下。

第一次更新的 baseState 是空字符串,更新队列如下,字母示意 state,数字示意优先级。优先级是 1 > 2 的

 A1 - B1 - C2 - D1 - E2
 
 第一次的渲染优先级(renderLanes)为 1,Updates 是本次会被解决的队列:
 Base state: ''
 Updates: [A1, B1, D1]      <- 第一个被跳过的 update 为 C2,此时的 baseUpdate 队列为[C2, D1, E2],它之前所有被解决的 update 的后果是 AB。此时记录下 baseState = 'AB'
                               留神!再次跳过低优先级的 update(E2)时,则不会记录 baseState
                               
 Result state: 'ABD'--------------------------------------------------------------------------------------------------
 
 
 第二次的渲染优先级(renderLanes)为 2,Updates 是本次会被解决的队列:
 Base state: 'AB'           <- 再次发动调度时,取出上次更新遗留的 baseUpdate 队列,基于 baseState
                               计算结果。Updates: [C2, D1, E2] Result state: 'ABCDE'

优先级足够

如果某个 update 优先级足够,次要是两件事:

  • 判断若 baseUpdate 队列不为空(之前有被跳过的 update),则将当初这个 update 放入 baseUpdate 队列。
  • 解决更新,计算新状态。

将优先级足够的 update 放入 baseUpdate 这一点能够和上边低优先级 update 入队 baseUpdate 联合起来看。这实际上意味着一旦有 update 被跳过,就以它为终点,将后边直到最初的 update 无论优先级如何都截取下来。再用上边的例子来阐明一下。

A1 - B2 - C1 - D2
B2 被跳过,baseUpdate 队列为
B2 - C1 - D2

这样做是为了保障最终全副更新实现的后果和用户行为触发的那些更新全副实现的预期后果保持一致。比方,A1 和 C1 尽管在第一次被优先执行,展示的后果为 AC,但这只是为了及时响应用户交互产生的长期后果,实际上 C1 的后果须要依赖 B2 计算结果,当第二次 render 时,根据 B2 的前序 update 的处理结果(baseState 为 A)开始解决 B2 – C1 – D2 队列,最终的后果是 ABCD。在提供的高优先级工作插队的例子中,能够证实这一点。

变动过程为 0 -> 2 -> 3,生命周期将 state 设置为 1(工作 A2),点击事件将 state + 2(工作 A1),失常状况下 A2 失常调度,然而未 render 实现,此时 A1 插队,更新队列 A2 – A1,为了优先响应高优先级的更新,跳过 A2 先计算 A1,数字由 0 变为 2,baseUpdate 为 A2 – A1,baseState 为 0。而后再重做低优先级工作。解决 baseUpdate A2 – A1,以 baseState(0)为根底进行计算,最初后果是 3。

实现阶段

次要是做一些赋值和优先级标记的工作。

  • 赋值 updateQueue.baseState。若此次 render 没有更新被跳过,那么赋值为新计算的 state,否则赋值为第一个被跳过的更新之前的 update。
  • 赋值 updateQueue 的 firstBaseUpdate 和 lastBaseUpdate,也就是如果本次有更新被跳过,则将被截取的队列赋值给 updateQueue 的 baseUpdate 链表。
  • 更新 workInProgress 节点的 lanes。更新策略为如果没有优先级被跳过,则意味着本次将 update 都解决完了,lanes 清空。否则将低优先级 update 的优先级放入 lanes。之前说过,

此处是再发动一次调度重做低优先级工作的要害。

  • 更新 workInProgress 节点上的 memoizedState。

源码实现

下面根本把解决更新的所有过程叙述了一遍,当初让咱们看一下源码实现。这部分的代码在 processUpdateQueue 函数中,它外面波及到了大量的链表操作,代码比拟多,
咱们先来看一下它的构造,我标注出了那三个阶段。

function processUpdateQueue<State>(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void {
 // 筹备阶段
 const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
 let firstBaseUpdate = queue.firstBaseUpdate;
 let lastBaseUpdate = queue.lastBaseUpdate;
 let pendingQueue = queue.shared.pending;
 if (pendingQueue !== null) {/* ... */}
 if (firstBaseUpdate !== null) { // 解决阶段
 do {...} while (true);
 // 实现阶段
 if (newLastBaseUpdate === null) {newBaseState = newState;} queue.baseState = ((newBaseState: any): State);
 queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes);
 workInProgress.lanes = newLanes; workInProgress.memoizedState = newState;
 }}

对于下面的概念与源码的主体构造理解之后,放出残缺代码,但删除了无关局部,我增加了正文,对照着那三个过程来看会更有助于了解,否则单看链表操作还是有些简单。

function processUpdateQueue<State>(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void {
 // 筹备阶段 ----------------------------------------
 // 从 workInProgress 节点上取出 updateQueue
 // 以下代码中的 queue 就是 updateQueue
 const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
 // 取出 queue 上的 baseUpdate 队列(上面称遗留的队列),而后
 // 筹备接入本次新产生的更新队列(上面称新队列)let firstBaseUpdate = queue.firstBaseUpdate;
 let lastBaseUpdate = queue.lastBaseUpdate;
 // 取出新队列
 let pendingQueue = queue.shared.pending;
 // 上面的操作,实际上就是将新队列连贯到上次遗留的队列中。if (pendingQueue !== null) { queue.shared.pending = null;
 // 取到新队列
 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next;
 // 将遗留的队列最初一个元素指向 null,实现断开环状链表
 // 而后在尾部接入新队列
 lastPendingUpdate.next = null; if (lastBaseUpdate === null) {firstBaseUpdate = firstPendingUpdate;} else { // 将遗留的队列中最初一个 update 的 next 指向新队列第一个 update
 // 实现接入
 lastBaseUpdate.next = firstPendingUpdate; } // 批改遗留队列的尾部为新队列的尾部
 lastBaseUpdate = lastPendingUpdate;
 // 用同样的形式更新 current 上的 firstBaseUpdate 和
 // lastBaseUpdate(baseUpdate 队列)。// 这样做相当于将本次合并实现的队列作为 baseUpdate 队列备份到 current 节
 // 点上,因为如果本次的渲染被打断,那么下次再从新执行工作的时候,workInProgress 节点复制
 // 自 current 节点,它下面的 baseUpdate 队列会保有这次的 update,保障 update 不失落。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;
 if (currentLastBaseUpdate !== lastBaseUpdate) {if (currentLastBaseUpdate === null) {currentQueue.firstBaseUpdate = firstPendingUpdate;} else {currentLastBaseUpdate.next = firstPendingUpdate;} currentQueue.lastBaseUpdate = lastPendingUpdate; } } }
 // 至此,新队列曾经合并到遗留队列上,firstBaseUpdate 作为
 // 这个新合并的队列,会被循环解决
 // 解决阶段 -------------------------------------
 if (firstBaseUpdate !== null) { // 取到 baseState
 let newState = queue.baseState;
 // 申明 newLanes,它会作为本轮更新解决实现的
 // 优先级,最终标记到 WIP 节点上
 let newLanes = NoLanes;
 // 申明 newBaseState,留神接下来它被赋值的机会,还有前置条件:// 1. 当有优先级被跳过,newBaseState 赋值为 newState,// 也就是 queue.baseState
 // 2. 当都解决实现后没有优先级被跳过,newBaseState 赋值为
 // 本轮新计算的 state,最初更新到 queue.baseState 上
 let newBaseState = null;
 // 应用 newFirstBaseUpdate 和 newLastBaseUpdate // 来示意本次更新产生的的 baseUpdate 队列,目标是截取现有队列中
 // 第一个被跳过的低优先级 update 到最初的所有 update,最初会被更新到
 // updateQueue 的 firstBaseUpdate 和 lastBaseUpdate 上
 // 作为下次渲染的遗留队列(baseUpdate)let newFirstBaseUpdate = null; let newLastBaseUpdate = null;
 // 从头开始循环
 let update = firstBaseUpdate; do { const updateLane = update.lane; const updateEventTime = update.eventTime; // isSubsetOfLanes 函数的意义是,判断以后更新的优先级(updateLane)// 是否在渲染优先级(renderLanes)中如果不在,那么就阐明优先级有余
 if (!isSubsetOfLanes(renderLanes, updateLane)) {
 const clone: Update<State> = {
 eventTime: updateEventTime,
 lane: updateLane,
 suspenseConfig: update.suspenseConfig,
 tag: update.tag,
 payload: update.payload,
 callback: update.callback,
 next: null,
 }; // 优先级有余,将 update 增加到本次的 baseUpdate 队列中
 if (newLastBaseUpdate === null) { newFirstBaseUpdate = newLastBaseUpdate = clone; // newBaseState 更新为前一个 update 工作的后果,下一轮
 // 持有新优先级的渲染过程解决更新队列时,将会以它为根底进行计算。newBaseState = newState; } else { // 如果 baseUpdate 队列中曾经有了 update,那么将以后的 update
 // 追加到队列尾部
 newLastBaseUpdate = newLastBaseUpdate.next = clone; } /* * * newLanes 会在最初被赋值到 workInProgress.lanes 上,而它又最终
 * 会被收集到 root.pendingLanes。* * 再次更新时会从 root 上的 pendingLanes 中找出渲染优先级(renderLanes),* renderLanes 含有本次跳过的优先级,再次进入 processUpdateQueue 时,* update 的优先级符合要求,被更新掉,低优先级工作因而被重做
 * */ newLanes = mergeLanes(newLanes, updateLane);
 } else {if (newLastBaseUpdate !== null) { // 进到这个判断阐明当初解决的这个 update 在优先级有余的 update 之后,// 起因有二:// 第一,优先级足够;// 第二,newLastBaseUpdate 不为 null 阐明曾经有优先级有余的 update 了
 // // 而后将这个高优先级放入本次的 baseUpdate,实现之前提到的从 updateQueue 中
 // 截取低优先级 update 到最初一个 update
 const clone: Update<State> = {
 eventTime: updateEventTime,
 lane: NoLane,
 suspenseConfig: update.suspenseConfig,
 tag: update.tag,
 payload: update.payload,
 callback: update.callback,
 next: null,
 }; newLastBaseUpdate = newLastBaseUpdate.next = clone; } markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig);
 // Process this update. // 解决更新,计算出新后果
 newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance,); const callback = update.callback;
 // 这里的 callback 是 setState 的第二个参数,属于副作用,// 会被放入 queue 的副作用队列里
 if (callback !== null) { workInProgress.effectTag |= Callback;
 const effects = queue.effects;
 if (effects === null) {queue.effects = [update]; } else {effects.push(update);
 } } } // 挪动指针实现遍历
 update = update.next; if (update === null) { // 已有的队列解决完了,检查一下有没有新进来的,有的话
 // 接在已有队列后边持续解决
 pendingQueue = queue.shared.pending;
 if (pendingQueue === null) { // 如果没有期待解决的 update,那么跳出循环
 break; } else { // 如果此时又有了新的 update 进来,那么将它接入到之前合并好的队列中
 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
 lastPendingUpdate.next = null; update = firstPendingUpdate; queue.lastBaseUpdate = lastPendingUpdate; queue.shared.pending = null; } } } while (true); // 如果没有低优先级的更新,那么新的 newBaseState 就被赋值为
 // 刚刚计算出来的 state
 if (newLastBaseUpdate === null) {newBaseState = newState;}
 // 实现阶段 ------------------------------------
 queue.baseState = ((newBaseState: any): State); queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes);
 workInProgress.lanes = newLanes; workInProgress.memoizedState = newState; }}

hooks 中 useReducer 解决更新计算状态的逻辑与此处根本一样。

总结

通过下面的梳理,能够看进去整个对更新的解决都是围绕优先级。整个 processUpdateQueue 函数要实现的目标是解决更新,但要保障更新依照优先级被解决的同时,不乱阵脚,这是因为它遵循一套固定的规定:优先级被跳过后,记住此时的状态和此优先级之后的更新队列,并将队列备份到 current 节点,这对于 update 对象按秩序、残缺地被解决至关重要,也保障了最终出现的处理结果和用户的行为触发的交互的后果保持一致。

欢送扫码关注公众号,发现更多技术文章

正文完
 0