阅读 9790

扒一扒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对象按次序、完整地被处理至关重要,也保证了最终呈现的处理结果和用户的行为触发的交互的结果保持一致。

文章分类
前端
文章标签