React Fiber - updateQueue 原理分析

1,345 阅读8分钟

React Fiber - updateQueue

在 react 中,class 组件以及使用 state hook 的函数组件可以拥有自己的状态。而对于 class 组件来说,每当状态改变时,会生成一个 update 存放在其对应 fiber 的 updateQueue 中。并且在 diff 时根据该 updateQueue 中的所有 update 计算出该组件的最新状态。

why queue

为什么 react 要设计一个 queue 来存放多个 update 而不是每当状态改变时直接将这个组件的状态设为最新的 update 呢?这里主要是为了服务批量更新。例如:

function onChange(){
    setState(1)
    setState(2)
    setState(3)
}

react 希望这几个状态能够合并在一起进行计算,而不是每 setState 就更新一次状态。因为更新状态意味着 fiber tree 的 diff,还有可能会发生真实 dom 的改变。这种耗费时间的操作能少做一次是一次。当有了 updateQueue 后,这种情况会以此存放在 queue 中。react 在一次 diff 中计算出一个最终状态,这样就解决了批量 state 的问题。

updateQueue 的结构

在谈到 updateQueue 的结构时,首先想到的就是环形链表:

a -> b -> c   // 普通链表

a -> b -> c   // 环形链表
^_________|

实际上使用环形链表的原因也很简单。普通链表如果只保存首节点的指针,则每次插入时需要先进行一次遍历找到尾节点,再进行插入。要么就需要保存首尾节点的指针。而环形链表的尾节点的下一个节点就是首节点,因此只需要保存一个尾节点就可以做到既方便插入又方便访问首节点。 updateQueue 在代码中的定义如下:

UpdateQueue<State> = {
  baseState: State,
  firstBaseUpdate: Update<State>,
  lastBaseUpdate: Update<State>,
  shared: SharedQueue<State>, // 真正存放 update 的链表的属性
  effects: Array<Update<State>>
};

type SharedQueue<State> = {
  pending: Update<State> | null,
};

updateQueue 中多个 update 组成的链表是存放在 shared.pending 属性上。除了这个属性外还有很多的属性,其作用会在后面讲到。每次有新的 update 时,会按照以下逻辑在 updateQueue 中新增:

const pending = sharedQueue.pending;
  if (pending === null) {
    // pending 为空,说明这是第一个 update
    update.next = update;
  } else {
    // 否则这个 update 会被添加到链表称为尾节点
    // 先将这个 update 指向头节点
    update.next = pending.next;
    // 再将当前的尾节点指向该节点
    pending.next = update;
  }
  // 将该 pending 设为该节点
  sharedQueue.pending = update;

可以看出,updateQueue.shared.pending 始终是指向链表的尾节点。例如我们存在 1,2,3 三个 update。那么其 shared 变化为:

shared
1  -> 
^___| 

    shared
1 -> 2
^____|
         shared      
1 -> 2 -> 3
^_________|

如何计算最终 state

有了 updateQueue 之后,还有一个重要的问题是如何根据 queue 计算最终的 update。先问一个问题,直接取 update 的最后一个 update 作为最终状态是否可行? 答案是: 否。原因也非常简单,因为在 react 中 update 可能为一个函数,例如:

    setState(1)
    setState(x => x + 1)
    setState(x => x + 2)

如果这种情况只取第三个 update 则为 x => x + 2。这显然是没有意义的,因此还是需要老老实实的从第一个 state 开始算起。当时就算如此状态的计算也是非常简单的,那么在上面提到的 baseState, firstBaseUpdate, lastBaseUpdate 的作用又是什么呢?

这三个属性的作用是为了实现 react fiber 提出的时间切片。熟悉操作系统的同学都知道,在任务调度算法中有时间分片的概念和这里的时间切片如出一辙。时间切片给 react 带来了基于任务优先级的可中断渲染。例如,在用户进行输入的时候,每次输入都可能引起整个 fiber 树的 diff。如果 diff 的时间过长,那么可能会阻塞用户的下一次输入。体现在用户的体验上就是掉帧。为了避免这种掉帧的情况,react 会将一个大的渲染过程分成多个时间片,当优先级较高的事件出现时中断当前的渲染过程,转而渲染优先级更高的任务,避免卡顿的产生。

而当这个过程体现在 updateQueue 上时,即为优先级较高的 update 可能会优先渲染。例如有下面两个优先级不同的 update 在初始状态为 1 的情况下产生:

initState: 0
a: setState(x => x + 1)  优先级: 高
b: setState(x => x + 2)  优先级: 低
c: setState(x => x + 3)  优先级: 高

由于 a, c 的优先级高于 b,react 希望能优先渲染 a, c。该轮渲染完成之后 b 这个 update 被跳过了。但是无论 update 被跳过多少个,都需要保证最终状态的正确性。在该例子中,无论跳过多少过 update,最终渲染的状态都必须是 6。因此下一次计算时还是需要从第一个 update 开始计算最终状态。不过这里有一个可以优化的地方: 第一个被跳过之前的最终状态是可以复用的,不必重复计算。例如例子里面每个 update 计算之后得到的最终状态为:

a -> 1
b -> 3 // 被跳过
c -> 6

那么下一次重新渲染之后,update a 就不需要在计算一次,因为在它之前没有任何被跳过的状态,react 只需基于update a 的最终状态 1 ,从 update b 开始算起即可,这种方式可以减少状态的重复计算。那么 firstBaseUpdate 则是指向被跳过的首个 update,在这个例子中则为 b。而 baseState 则是指向被跳过的首个元素之前的所有 update 计算出的最终状态,在这个例子中为 1。至于 lastBaseUpdate,只要有 update 被跳过,那么它一定指向本次更新的 update 中的最后一个,在这个例子中为 c。

在搞清楚这几个属性的含义后,现在又有了一个新的问题。如果上一次存在更新被跳过,下一次又有新的更新出现。例如上面的例子渲染之后,又出现了 d, e 两个更新,这时候 react 会怎么处理。先回忆一下,在之前已经提到过 updateQueue 中的 update queue 是以环形链表存放在 shard.pending 上的,因此现在我们得到两个链表:

// 由 firstBaseUpdate 以及 lastBaseUpdate 形成的链表,这个链表不是环形的,具体原因后面会讲到
b -> c

// 由 shard.pending 形成的环型链表
d -> e
^____|

此时,react 会将第二个环形链表剪开得到本次的 update 链表:

b -> c -> d -> e

并根据 baseState 计算最终状态。

源码解读

在 react 中通过processUpdateQueue 来完成状态计算的的逻辑,源码如下(删除了一些与更新无关的代码),注释中以 baseUpdate 表示由 firstBaseUpdate -> lastBaseUpdate 组成的链表:

export function processUpdateQueue<State>(
    workInProgress: Fiber,
    props: any,
    instance: any,
    renderLanes: Lanes,
  ): void {
  
    const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
  
    hasForceUpdate = false;

    let firstBaseUpdate = queue.firstBaseUpdate; 
    let lastBaseUpdate = queue.lastBaseUpdate;
  
    let pendingQueue = queue.shared.pending;
    // 如果 pending 不为空的情况需要剪开环型链表并合并
    if (pendingQueue !== null) { 
        // 将 pending 设为空,表示这些 pending 已经处理过
      queue.shared.pending = null; 
      const lastPendingUpdate = pendingQueue; // pending 的最后一个 udpdate
      const firstPendingUpdate = lastPendingUpdate.next; // pending 的第一个 update
      // 剪开环,使最后一个 update 不再指向第一个 update
      lastPendingUpdate.next = null; 
      if (lastBaseUpdate === null) { 
        firstBaseUpdate = firstPendingUpdate;
      } else {
        // 如果上一次有跳过的 update,那么 baseUpdate 链表不为空
        // 需要将 pending 的第一个 update 接上 baseUpdate
        lastBaseUpdate.next = firstPendingUpdate;
      }
      // 将 lastBaseUpdate 赋值为 lastPendingUpdate 
      // 此时已经形成了 以 firstBaseUpdate 为头以 lastBaseUpdate 为尾的新链表
      // 也即为本次需要处理的 update 链表
      lastBaseUpdate = lastPendingUpdate;
    }
  
    if (firstBaseUpdate !== null) {
      let newState = queue.baseState; 
      let newLanes = NoLanes;
      // 这里的 newBaseState, newFirstBaseUpdate,newLastBaseUpdate 是计算的临时变量
      // 实际上会用来更新 updateQueue 的 baseState, firstBaseUpdate, lastBaseUpdate
      let newBaseState = null;
      let newFirstBaseUpdate = null;
      let newLastBaseUpdate = null;
      let update = firstBaseUpdate;
      do {
        const updateLane = update.lane;
        const updateEventTime = update.eventTime;
        // 更新优先级不满足,该 update 会被跳过
        if (!isSubsetOfLanes(renderLanes, updateLane)) {
          const clone: Update<State> = {
            eventTime: updateEventTime,
            lane: updateLane,

            tag: update.tag,
            payload: update.payload,
            callback: update.callback,
  
            next: null, // 注意这里的 next 设置为 null,
            // 因此由 firstBaseUpdate 以及 lastBaseUpdate 组成的链表不是环形的
          };
          // 如果 newLastBaseUpdate 为空,说明这是第一个被跳过的 update
          // 因此 newFirstBaseUpdate 为该 update
          if (newLastBaseUpdate === null) {
            newFirstBaseUpdate = newLastBaseUpdate = clone;
            // 同时表明在该 update 之前没有任何 upadte 被跳过
           // 需要即记录第一个跳过的 update 之前的最终状态
            newBaseState = newState; 
          } else {
            // 否则直接将该 update 添加到 baseUpdate 链表最后,等价于
            // newLastBaseUpdate.next = clone
            // newLastBaseUpdate = newLastBaseUpdate.next
            newLastBaseUpdate = newLastBaseUpdate.next = clone;
          }
          // Update the remaining priority in the queue.
          newLanes = mergeLanes(newLanes, updateLane);
        } else {
          // 该 update 更新优先级满足,本次更新不会跳过
          // 如果 newLastBaseUpdate 不存在,说明之前没有跳过任何 upadte 无需添加新增
          // 否则无论无论该 update 是否跳过都需要添加到 baseUpdate 链表之后
          if (newLastBaseUpdate !== null) {
            const clone: Update<State> = {
              eventTime: updateEventTime,
              // 这个 update 本次不会跳过,因此将其优先级设置为最高
              // 后续的更新计算一定不会跳过该 update 
              lane: NoLane, 
  
              tag: update.tag,
              payload: update.payload,
              callback: update.callback,
  
              next: null, // 注意这里的 next 设置为 null,
              // 因此由 firstBaseUpdate 以及 lastBaseUpdate 组成的链表不是环形的
            };
            newLastBaseUpdate = newLastBaseUpdate.next = clone;
          }
  
          // 计算最新的 state.
          newState = getStateFromUpdate(
            workInProgress,
            queue,
            update,
            newState,
            props,
            instance,
          );
        }
        update = update.next;
        if (update === null) {
          pendingQueue = queue.shared.pending;
          if (pendingQueue === null) { // 计算到链表尾部,退出
            break;
          }
        }
      } while (true);
  
      if (newLastBaseUpdate === null) {
        newBaseState = newState;
      }
      
      // 更新 updateQueue 的 baseState,firstBaseUpdate, lastBaseUpdate 三个属性
      queue.baseState = ((newBaseState: any): State); 
      queue.firstBaseUpdate = newFirstBaseUpdate;
      queue.lastBaseUpdate = newLastBaseUpdate;
    }
  }

虽然代码很长,但是逻辑其实比较简单,主要包含以下逻辑:

  1. 如果 baseUpdate 链存在,需要则剪开环形链表和 baseUpdate 链表合并。
  2. 遍历每个 update,判定哪些 update 需要更新,计算最新状态。并同时更新 baseUpdate 链表以及 baseState。

这里还有一点需要注意的是,在代码的最开始存在一个判断 if(pendingQueue !== null)。这就意味着当 shared.pending 为空时,由于上一次可能还存在需要处理的 state,所以该组件仍然可能被更新。