React状态计算解密

点击进入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);
 
 // 处理更新,计算出新结果
 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对象按次序、完整地被处理至关重要,也保证了最终呈现的处理结果和用户的行为触发的交互的结果保持壹致。


<<:  laravel 8 (一) 建立专案及资料库设定

>>:  laravel 8部署到免费的Heroku

D5 第二周 程序基础

今天整理程序的基础知识,所有的程序都是由这些基础建构起来的。 我觉得程序跑起来真正重要的观念大概有三...

Day 05 - jS 微基础之ES6回圈:loop

loop(回圈)意味着在有限的条件下,帮忙重复做多少事的意思。 以下举基本的for回圈为范例: fo...

Ruby on Rails 局部渲染(Partial Render)

局部渲染是⼀种在 Rails 专案很常⾒的程序码整理⼿法之⼀。在前⼀个章节整理表单 的时候,有⽤到了...

初探 OpenTelemetry

为什麽会接触到 OpenTelemetry,算是因为 Log 的追踪关系,在後台上有两三个 Spri...

从 React 开始,让你的网页material-ui起来 [Day 4] 排版布局Grid

布局排板大板型左右留边 Container 接下来就是这个Container里头需要载运那些内容了 ...