Bootstrap

React 的源码与原理解读(十):updateQueue 与 processUpdateQueue

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

这个章节是补充的章节,因为我突然发现将了 lanes 的时候提到了我们的 updateQueue ,我以前之前讲过了,结果发现之前因为涉及了大量 lanes 的内容所以没讲,那么在将 hooks 之前,我们补充一篇 updateQueue 以及相关操作的讲解,这篇会让你在我学习**lanes ** 系统之后,对于我们怎么样依赖 lanes 系统进行我们的更新有更深刻的了解,这里涉及到一道非常经典的面试题,我们也会提到:

updateQueue

我们首先来回顾一下,updateQueue 是什么,我们回到我们的 fiber 结构中,可以看到,每一个 fiber 节点都有一个 updateQueue 的更新队列,我们先来看看这个更新队列的数据结构,它在源码的这个位置:packages/react-reconciler/src/ReactUpdateQueue.old.js

export type UpdateQueue<State> = {|
  baseState: State,      // 当前 state
  firstBaseUpdate: Update<State> | null,  // 上次渲染时遗的链表头节点
  lastBaseUpdate: Update<State> | null,   // 上次渲染时遗的链表尾节点
  shared: SharedQueue<State>, // 本次渲染时要执行的任务,
  effects: Array<Update<State>> | null, // 有回调函数的update
};

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState, // 前一次更新计算得出的状态,
    firstBaseUpdate: null, 
    lastBaseUpdate: null, 
    shared: {
      pending: null, // 更新操作的循环链表
      interleaved: null,
      lanes: NoLanes,
    },
    effects: null,
  };
  fiber.updateQueue = queue;
}

我们先来看很好理解的一部分内容:

  • baseState 这个存放我们渲染开始前 state 的值
  • shared 是一个数据结构,里面存放了我们的一系列更新,我们需要根据 shared 的更新和 baseState 算出我们更新后的值
  • effects 存放是有回调函数的更新,因为更新完毕后我们还需要触发他们的回调
  • 最后我们来看 firstBaseUpdate 和 lastBaseUpdate ,这里要用到我们 lanes 的知识,我们每次调度时都会判断当前任务是否有足够的优先级来执行,若优先级不够,则重新存储到链表中,用于下次渲染时重新调度,所以我们在新一次调度的时候,需要先解决这些遗留的任务,再开始我们新的任务

在我们创建 Fiber 节点的时候,我们可以使用 initializeUpdateQueue 来创建我们的 UpdateQueue ,使用的是 Fiber 里面的 memoizedState 属性来创建我们的 baseState

update

之后我们再来看看单个的 update 更新,这也是我们的老朋友了,之前多次提到过,因为我们的 UpdateQueue 是一个链表,所以我们的需要在我们 单个 Update 中放上一个 next 指针指向我们的下一个节点:

const update: Update<*> = {
  eventTime, // 当前操作的时间
  lane, // 优先级
  tag: UpdateState, // 执行的操作
  payload: null,
  callback: null,
  next: null, // next指针
};

我们主要关注这些参数:

  • eventTime:任务时间,通过 performance.now() 获取的毫秒数。

  • lane:优先级相关字段。

  • tag:更新的类型,包括 UpdateState | ReplaceState | ForceUpdate | CaptureUpdate。

  • payload:更新挂载的数据,不同类型组件挂载的数据不同。对于 ClassComponent,payload 为 this.setState的第一个传参。对于 HostRoot,payload 为 ReactDOM.render 的第一个传参。

  • callback:更新的回调函数,也就是 setState 的第二个参数。

  • next:与其他 Update 连接形成链表,例如如果同时触发多个 setState 时会形成多个 Update,然后通过 next 连接。

这些参数在我们的更新创建过程中都提到过,我们就不再阐述了,我们继续看相关的操作:

enqueueUpdate

这个函数用于将我们的更新添加到我们的 updateQueue 中,我们简单来看一下这个逻辑:

  • 首先我们拿到我们的 updateQueue
  • 我们拿到我们的 updateQueue 的 shared 的 pending ,这个属性是我们更新队列挂在的位置
  • InterleavedUpdate 是进行交错更新的处理,在渲染过程中途出现的 update,被称为交错更新,在更新队列中,有两个单链表队列字段:pending 和 interleaved 。在我们调度一个交错更新 update 时,它会被储存在 interleaved 属性中。然后整个字段都会被推送到一个全局数组变量上。在当前渲染结束之后,遍历全局数组变量,将交错更新转移到 pending 队列中。
  • 当我们插入一个更新的时候,当已经存在节点时,pending 指向的是最后一个节点,pending.next 是指向的第一个节点,我们插入一个节点后, 让 update 的 next 指向到了第一个节点, 最后一个节点 pending 的 next 指针指向到了update节点,这样update就进入到链表中了,此时 update 是链表的最后一个节点了
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>, lane: Lane) {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // 只有在fiber已经被卸载了才会出现
    return;
  }
  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
  // 交错更新
  if (isInterleavedUpdate(fiber, lane)) {
   if (interleaved === null) {
      update.next = update;
      pushInterleavedQueue(sharedQueue);
    } else {
      update.next = interleaved.next;
      interleaved.next = update;
    }
    sharedQueue.interleaved = update;

  } else {
    const pending = sharedQueue.sharedQueue;

    if (pending === null) {
      // 第一个节点
      update.next = update;
    } else {
      // 链表中有数据了,添加数据
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
  }
}

我们可以看到,我们的 sharedQueue 中的 pending 队列是一个环形队列,使用这种数据结构的原因是:

  • 我们只需要操作更新队列的头部和尾部来实现队列的合并,以及遍历我们的队列,链表就可以完成我们需要的功能

  • 链表可以让我们非常快速的插入一个新的更新对象,不会造成空间的浪费

  • 环形链表可以只需要利用一个指针,便能找到最后一个和第一个节点,而普通的链表需要同时维护头节点和尾节点,否则合并操作的效率很低

processUpdateQueue

之后我们的重头戏来了,我们现在可以创建和添加我们的更新,那么更新是怎么样作用的我们的元素上的呢,它在 processUpdateQueue 这个函数上,我们来看看它的逻辑:

那么先总结一波,processUpdateQueue 函数做了这几件事:

  • 我们先把我们的环形链表拆开;然后把 firstBaseUpdatelastBaseUpdate 构成的队列拼接到 queue 前面,最后构成一个大的线性 UpdateQueuefirstBaseUpdatelastBaseUpdate 分别作为队列的起点和终点
  • workInProgress 节点中的 queue 同步到 current 节点
  • 遍历我们的队列,依次判断出每个 updatelane 是否满足更新队列的优先级(这个我们在上一篇说过)
  • 如果不满足更新优先级条件,把不满足更新条件的 update 用链表存了起来,newFirstBaseUpdatenewLastBaseUpdate 是队列的起点和终点,如果出现有 update 被延迟执行,那么会把当前已经计算好的 newState 先做一次保存,然后更新 newLanes ,这是下一次执行的 lanes,往里塞入了满足条件的优先级,这样下次遍历到时才能执行
  • 如果满足更新优先级条件,首先判断前面如果出现过有 update 被推迟那么后面所有任务都必须进入到被延迟的队列中,因为前一个任务可能会影响到后一个任务的,并且被推迟的 Update 对象的 lane 会被设置为 NoLane 等级,因为这个优先级会在检测是否能运行时判定为永远都会为真,毕竟我们的任务本来就可以执行了。
  • 然后通过 getStateFromUpdate 计算新的 state,存放在 newState;然后保存 setStatecallback,就是第二个参数,接着标记当前 fibercallback,存放在 flags 字段中
  • 如果没出现 update 被延迟的情况下,把整个队列的计算结果 newState 赋值给 queue.baseState
  • 如果有被延迟的 update,标记哪些区间的 update 被延迟了,只更新 workInProgress(新节点)的 memoizedState
  • 这一轮的运行结束之后,我们就可以再次开始一轮新的调度继续运行我们下一次的批量更新,这部分我们在之前的教程中提到了
export function processUpdateQueue<State>(
  workInProgress: Fiber,
  props: any,
  instance: any,
  renderLanes: Lanes,
): void {
  const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);

  // 获取上一次还没有渲染的队列 firstBaseUpdate 和 lastBaseUpdate
  let firstBaseUpdate = queue.firstBaseUpdate;
  let lastBaseUpdate = queue.lastBaseUpdate;
  
  //获取当前的队列
  let pendingQueue = queue.shared.pending;

  // 将 pendingQueue 拼接到,更新链表 queue.firstBaseUpdate 的后面,我们先处理遗留的,再处理当前的
  if (pendingQueue !== null) {
    queue.shared.pending = null;
    const lastPendingUpdate = pendingQueue;
    const firstPendingUpdate = lastPendingUpdate.next;
    lastPendingUpdate.next = null;
    if (lastBaseUpdate === null) {
      firstBaseUpdate = firstPendingUpdate;
    } else {
      lastBaseUpdate.next = firstPendingUpdate;
    }
    lastBaseUpdate = lastPendingUpdate;

    // 若workInProgress 树对应的在 current 树的那个 fiber 节点存在,
    const current = workInProgress.alternate;
    if (current !== null) {
      const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
      const currentLastBaseUpdate = currentQueue.lastBaseUpdate;

      // 若current更新链表的最后那个节点与当前将要更新的链表的最后那个节点不一样则,把将要更新的链表也拼接到current中
      if (currentLastBaseUpdate !== lastBaseUpdate) {
        if (currentLastBaseUpdate === null) {
          currentQueue.firstBaseUpdate = firstPendingUpdate;
        } else {
          currentLastBaseUpdate.next = firstPendingUpdate;
        }
        currentQueue.lastBaseUpdate = lastPendingUpdate;
      }
    }
  }

  if (firstBaseUpdate !== null) {
    // 新的 state
    let newState = queue.baseState;
    // 新的 lane 优先级
    let newLanes = NoLanes;
    let newBaseState = null;
    let newFirstBaseUpdate = null;
    let newLastBaseUpdate = null;
    // 第一个 update
    let update = firstBaseUpdate;

    // 队列的循环处理
    do {
      const updateLane = update.lane;
      const updateEventTime = update.eventTime;
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 判断出当前 update 的 lane 不满足更新优先级条件,把不满足更新条件的 update 用存了起来
        const clone: Update<State> = {
          eventTime: updateEventTime,
          lane: updateLane,
          tag: update.tag,
          payload: update.payload,
          callback: update.callback,
          next: null,
        };
        if (newLastBaseUpdate === null) {
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          // 如果出现有 update 被延迟执行,把当前已经计算好的 newState 先做一次保存
          newBaseState = newState;
        } else {
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }
        // 更新 update 的 lane,使其下次遍历到时才能执行
        newLanes = mergeLanes(newLanes, updateLane);
      } else {
        // 满足更新条件的 update
        if (newLastBaseUpdate !== null) {
          //如果前面出现过有 update 被推迟,那么后面所有任务都必须进入到被延迟的队列中
          const clone: Update<State> = {
            eventTime: updateEventTime,
            lane: NoLane,
            tag: update.tag,
            payload: update.payload,
            callback: update.callback,
            next: null,
          };
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }

        // 计算新的 state
        newState = getStateFromUpdate(
          workInProgress,
          queue,
          update,
          newState,
          props,
          instance,
        );
        // 保存 setState 的 callback,就是第二个参数
        const callback = update.callback;
        if (callback !== null) {
          // 标记当前 fiber 有 callback
          workInProgress.flags |= Callback;
          const effects = queue.effects;
          if (effects === null) {
            queue.effects = [update];
          } else {
            effects.push(update);
          }
        }
      }
      // 下一个 update 对象
      update = update.next;
      if (update === null) {
        pendingQueue = queue.shared.pending;
        if (pendingQueue === null) {
          // 循环处理结束
          break;
        } else {
          // 当前的 queue 处理完后,需要检查一下 queue.shared.pending 是否有更新,如果有更新那么把新的放进来继续
          const lastPendingUpdate = pendingQueue;
          const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
          lastPendingUpdate.next = null;
          update = firstPendingUpdate;
          queue.lastBaseUpdate = lastPendingUpdate;
          queue.shared.pending = null;
        }
      }
    } while (true);

    if (newLastBaseUpdate === null) {
      // 没出现 update 被延迟的情况下,把的计算结果赋值给 newBaseState
      newBaseState = newState;
    }
    // 把 newBaseState 给到 baseState
    queue.baseState = ((newBaseState: any): State);

    // 保存被延迟的 update
    queue.firstBaseUpdate = newFirstBaseUpdate;
    queue.lastBaseUpdate = newLastBaseUpdate;
    
    // 交错更新
    const lastInterleaved = queue.shared.interleaved;
    if (lastInterleaved !== null) {
      let interleaved = lastInterleaved;
      do {
        newLanes = mergeLanes(newLanes, interleaved.lane);
        interleaved = ((interleaved: any).next: Update<State>);
      } while (interleaved !== lastInterleaved);
    } else if (firstBaseUpdate === null) {
      queue.shared.lanes = NoLanes;
    }

    // 标记哪些区间的update被延迟了
    markSkippedUpdateLanes(newLanes);
    workInProgress.lanes = newLanes;
    // 更新 workInProgress(新节点)的 memoizedState
    workInProgress.memoizedState = newState;
  }
}

我们来看看这个 getStateFromUpdate 函数:

  • 首先根据我们的更新设置的 tag 来判断我们需要的模式
  • 如果是 ReplaceState ,也就是我们调用 replaceState ,说明我们要舍弃掉旧状态,直接用新状态替换到旧状态,如果我们的 payload 是一个函数,我们传入我们的原始 state ,得到我们的下一个 state ;否则我们直接返回
  • 如果是 UpdateState,也就是我们调用 setState,我们需要将新旧状态合并,我们先得到我们的新 state,然后将其与之前的 state 数据进行合并
  • 如果是 ForceUpdate ,也就是我们使用了 forceUpdate 创建了更新, 我们直接将 hasForceUpdate 设置为 true,返回的还是旧状态,这个 hasForceUpdate 后续会在我们判断是否需要更新我们的组件的使用,比如在 updateClassComponent 中会进行判定,巨头可以查看之前的代码
function getStateFromUpdate<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  update: Update<State>,
  prevState: State,
  nextProps: any,
  instance: any,
): any {
  /**
   * 可以看到下面也是区分了几种情况
   *  ReplaceState:舍弃掉旧状态,直接用新状态替换到旧状态;
   *  UpdateState:新状态和旧状态的数据合并后再返回;
   *  ForceUpdate:只修改 hasForceUpdate 为true,不过返回的还是旧状态;
   */
  switch (update.tag) {
    case ReplaceState: {
      const payload = update.payload;
      if (typeof payload === 'function') {
        // 若 payload 是 function,则将 prevState 作为参数传入,执行payload()
        const nextState = payload.call(instance, prevState, nextProps);
        return nextState;
      }
      return payload;
    }
    case CaptureUpdate: {
      workInProgress.flags = (workInProgress.flags & ~ShouldCapture) | DidCapture;
    }
    case UpdateState: {
      const payload = update.payload;
      let partialState; // 用于存储计算后的新state结果
      if (typeof payload === 'function') {
        // 若payload是function,则将prevState作为参数传入,执行payload()
        partialState = payload.call(instance, prevState, nextProps);
      } else {
        // 若 payload 是变量,则直接赋值
        partialState = payload;
      }
      if (partialState === null || partialState === undefined) {
        // 若得到的结果是null或undefined,则返回之前的数据
        return prevState;
      }
      // 与之前的state数据进行合并
      return assign({}, prevState, partialState);
    }
    case ForceUpdate: {
      hasForceUpdate = true;
      return prevState;
    }
  }
  return prevState;
}

总结与拓展&经典面试题

我们刚刚讲解了 updateQueue 的相关操作,总结就是:

  • 我们使用 updateQueue 来存储我们的更新,其中存储了我们的每一个更新,更新可能由 setState replaceState 或者 forceUpdate 等函数通过 enqueueUpdate 创建
  • 当我们开始一个更新的时候,我们会通过 processUpdateQueue 来遍历我们的 updateQueue ,更新我们的 state
  • 其中因为我们的 lanes 优先级,我们的任务调度过程中,更新任务分为了两个两个部分,一个是优先级足够的,一个是不够的。
  • 对于优先级不够的任务以及它之后的任务,我们把它们暂存起来,下一次调度再处理他们
  • 对于优先级足够的任务,我们通过 getStateFromUpdate 来获取更新后的数据,跟我我们调用的模式,决定我们将两个 state 合并还是替换
  • 如果如果设定了强制更新模式( ForceUpdate创建的更新任务 ),我们设定标志位,之后在 render 阶段会进行更新
  • 否则,我们暂存本次更新的结果,但是不进行更新,继续我们的下一次调度,只有在全部任务更新完毕没有延迟任务的时候才会更新我们的 state

那么经过前面的说法,你肯定可以解答这个面试题了:

setState 是异步的还是同步的,为什么?

根据前面的讲解,我们的 setState 创建的更新不是每创建一个更新就执一次的,而且每次处理一批的更新,所以它的执行的异步的,你创建的更新不一定马上能得到响应。

所以 React 提供了两个方法来解决这个问题,一个是 forceUpdate ,我们通过 forceUpdate 设定其为强制更新

还有一个回调函数,我们可以看到,如果我们的由相关的回调函数,我们会把它放在 effects 队列中,接着标记当前 fibercallback,这个部分会在之后的过程中执行,此时我们已经更新了我们的 state,所以我们可以获得正确的 state 了

ok,五一期间补充了这个教程,之后我们会按照原先的计划用几篇来将讲我们的 Hooks

顺带预告一下,在写完 React 源码的教程之后我们会开一个 React SSR 的相关项目

;