乐趣区

关于javascript:React中的高优先级任务插队机制

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

在 React 的 concurrent 模式下,低优先级工作执行过程中,一旦有更高优先级的工作进来,那么这个低优先级的工作会被勾销,优先执行高优先级工作。等高优先级工作做完了,低优先级工作会被从新做一遍。

咱们用一个具体的例子来了解一下高优先级工作插队。

有这样一个组件,state 为 0,进入页面,会调用 setState 将 state 加 1,这个作为低优先级工作。React 开始进行更新,在这个低优先级工作尚未实现时,模仿按钮点击,state 加 2,这个作为高优先级工作。能够看到,页面上的数字变动为 0 -> 2 -> 3,而不是 0 -> 1 -> 3。这就阐明,当低优先级工作(加 1)正在进行时,高优先级工作进来了,而它会把 state 设置为 2。因为高优先级工作的插队,设置 state 为 1 的低优先级工作会被勾销,先做高优先级工作,所以数字从 0 变成了 2。而高优先级工作实现之后,低优先级工作会被重做,所以 state 再从 2 加到了 3。

景象如下:

利用 chrome 的性能剖析工具捕获更新过程,能够显著看到优先级插队的过程

残缺的 profile 文件我保留下来了,能够载入到 chrome 中具体查看:高优先级插队.json。

能够再看一下这个过程中两个工作优先级在调度过程中的信息

点击查看 高优先级插队示例代码文件。

点击查看 低优先级工作饥饿问题示例代码文件。

接下来咱们就来从 setState 开始,探讨一下这种插队行为的实质,内容波及 update 对象的生成、发动调度、工作循环、高优工作插队、update 对象的解决、低优先级工作重做等内容。

产生更新

当调用 setState 时,意味着组件对应的 fiber 节点产生了一个更新。setState 实际上是生成一个 update 对象,调用 enqueueSetState,将这个 update 对象连接到 fiber 节点的 updateQueue 链表中.

Component.prototype.setState = function(partialState, callback) {this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

enqueueSetState 的职责是创立 update 对象,将它入队 fiber 节点的 update 链表(updateQueue),而后发动调度。

  enqueueSetState(inst, payload, callback) {
    // 获取以后触发更新的 fiber 节点。inst 是组件实例
    const fiber = getInstance(inst);
    // eventTime 是以后触发更新的工夫戳
    const eventTime = requestEventTime();
    const suspenseConfig = requestCurrentSuspenseConfig();

    // 获取本次 update 的优先级
    const lane = requestUpdateLane(fiber, suspenseConfig);

    // 创立 update 对象
    const update = createUpdate(eventTime, lane, suspenseConfig);

    // payload 就是 setState 的参数,回调函数或者是对象的模式。// 解决更新时参加计算新状态的过程
    update.payload = payload;

    // 将 update 放入 fiber 的 updateQueue
    enqueueUpdate(fiber, update);

    // 开始进行调度
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }

梳理一下 enqueueSetState 中具体做的事件:

找到 fiber

首先获取产生更新的组件所对应的 fiber 节点,因为产生的 update 对象须要放到 fiber 节点的 updateQueue 上。而后获取以后这个 update 产生的工夫,这与更新的饥饿问题相干,咱们暂且不思考,而且下一步的 suspenseConfig 能够先疏忽。

计算优先级

之后比拟重要的是计算以后这个更新它的优先级 lane:

const lane = requestUpdateLane(fiber, suspenseConfig);

计算这个优先级的时候,是如何决定依据什么货色去计算呢?这还得从 React 的合成事件说起。

事件触发时,合成事件机制调用 scheduler 中的 runWithPriority 函数,目标是以该交互事件对应的事件优先级去派发真正的事件流程。runWithPriority 会将事件优先级转化为 scheduler 外部的优先级并记录下来。当调用 requestUpdateLane 计算 lane 的时候,会去获取 scheduler 中的优先级,以此作为 lane 计算的根据。

这部分的源码在这里

创立 update 对象,入队 updateQueue

依据 lane 和 eventTime 还有 suspenseConfig,去创立一个 update 对象,构造如下:

const update: Update<*> = {
  eventTime,
  lane,
  suspenseConfig,
  tag: UpdateState,
  payload: null,
  callback: null,
  next: null,
};
  • eventTime:更新的产生工夫
  • lane:示意优先级
  • suspenseConfig:工作挂起相干
  • tag:示意更新是哪种类型(UpdateState,ReplaceState,ForceUpdate,CaptureUpdate)
  • payload:更新所携带的状态。

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

再之后就是去调用 React 工作执行的入口函数:scheduleUpdateOnFiber去调度执行更新工作了。

当初咱们晓得了,产生更新的 fiber 节点上会有一个 updateQueue,它蕴含了刚刚产生的 update。上面该进入 scheduleUpdateOnFiber 了,开始进入真正的调度流程。通过调用scheduleUpdateOnFiber,render 阶段的构建 workInProgress 树的工作会被调度执行,这个过程中,fiber 上的 updateQueue 会被解决。

调度筹备

React 的更新入口是scheduleUpdateOnFiber,它辨别 update 的 lane,将同步更新和异步更新分流,让二者进入各自的流程。但在此之前,它会做几个比拟重要的工作:

  • 查看是否是有限更新,例如在 render 函数中调用了 setState。
  • 从产生更新的节点开始,往上始终循环到 root,目标是将 fiber.lanes 始终向上收集,收集到父级节点的 childLanes 中,childLanes 是辨认这个 fiber 子树是否须要更新的要害。
  • 在 root 上标记更新,也就是将 update 的 lane 放到 root.pendingLanes 中,每次渲染的优先级基准:renderLanes 就是取自 root.pendingLanes 中最紧急的那一部分 lanes。

这三步能够视为更新执行前的筹备工作。

第 1 个能够避免死循环卡死的状况。

第 2 个,如果 fiber.lanes 不为空,则阐明该 fiber 节点有更新,而 fiber.childLanes 是判断以后子树是否有更新的重要依据,若有更新,则持续向下构建,否则间接复用已有的 fiber 树,就不往下循环了,能够屏蔽掉那些无需更新的 fiber 节点。

第 3 个是将以后 update 对象的 lane 退出到 root.pendingLanes 中,保障真正开始做更新工作的时候,获取到 update 的 lane,从而作为本次更新的渲染优先级(renderLanes),去更新。

实际上在更新时候获取到的 renderLanes,并不一定蕴含 update 对象的 lane,因为有可能它只是一个较低优先级的更新,有可能在它后面有高优先级的更新

梳理完 scheduleUpdateOnFiber 的大抵逻辑之后,咱们来看一下它的源码:

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // 第一步,查看是否有有限更新
  checkForNestedUpdates();

  ...
  // 第二步,向上收集 fiber.childLanes
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);

  ...

  // 第三步,在 root 上标记更新,将 update 的 lane 放到 root.pendingLanes
  markRootUpdated(root, lane, eventTime);

  ...

  // 依据 Scheduler 的优先级获取到对应的 React 优先级
  const priorityLevel = getCurrentPriorityLevel();

  if (lane === SyncLane) {
    // 本次更新是同步的,例如传统的同步渲染模式
    if ((executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // 如果是本次更新是同步的,并且以后还未渲染,意味着主线程闲暇,并没有 React 的
      // 更新工作在执行,那么调用 performSyncWorkOnRoot 开始执行同步工作

      ...

      performSyncWorkOnRoot(root);
    } else {
      // 如果是本次更新是同步的,不过以后有 React 更新工作正在进行,// 而且因为无奈打断,所以调用 ensureRootIsScheduled
      // 目标是去复用曾经在更新的工作,让这个已有的工作
      // 把这次更新顺便做了
      ensureRootIsScheduled(root, eventTime);
      ...
    }
  } else {

    ...

    // Schedule other updates after in case the callback is sync.
    // 如果是更新是异步的,调用 ensureRootIsScheduled 去进入异步调度
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }

  ...
}

scheduleUpdateOnFiber 的残缺源码在这里,这里是第二步:markUpdateLaneFromFiberToRoot 和 第三步:markRootUpdated 的残缺源码,我都做了正文。

通过了后面的筹备工作后,scheduleUpdateOnFiber最终会调用 ensureRootIsScheduled,来让 React 工作被调度,这是一个十分重要的函数,它关乎 等同或较低工作的收敛
高优先级工作插队 工作饥饿问题,上面具体解说它。

开始调度

在开始解说 ensureRootIsScheduled 之前,咱们有必要弄清楚 React 的更新工作的实质。

React 工作的实质

一个 update 的产生最终会使 React 在内存中依据现有的 fiber 树构建一棵新的 fiber 树,新的 state 的计算、diff 操作、以及一些生命周期的调用,都会在这个构建过程中进行。这个整体的构建工作被称为 render 阶段,这个 render 阶段整体就是一个残缺的 React 更新工作,更新工作能够看作执行一个函数,这个函数在 concurrent 模式下就是performConcurrentWorkOnRoot,更新工作的调度能够看成是这个函数被 scheduler 依照工作优先级安顿它何时执行。

Scheduler 的调度和 React 的调度是两个齐全不同的概念,React 的调度是协调工作进入哪种 Scheduler 的调度模式,它的调度并不波及工作的执行,而 Scheduler 是调度机制的真正外围,它是实打实地去执行工作,没有它,React 的工作再重要也无奈执行,心愿读者加以辨别这两种概念。

当一个工作被调度之后,scheduler 就会生成一个工作对象(task),它的构造如下所示,除了 callback 之外临时不须要关怀其余字段的含意。

  var newTask = {
    id: taskIdCounter++,
    // 工作函数,也就是 performConcurrentWorkOnRoot
    callback,
    // 任务调度优先级,由行将讲到的工作优先级转化而来
    priorityLevel,
    // 工作开始执行的工夫点
    startTime,
    // 工作的过期工夫
    expirationTime,
    // 在小顶堆工作队列中排序的根据
    sortIndex: -1,
  };

每当生成了一个这样的工作,它就会被挂载到 root 节点的 callbackNode 属性上,以示意以后曾经有工作被调度了,同时会将工作优先级存储到 root 的 callbackPriority 上,
示意如果有新的工作进来,必须用它的工作优先级和已有工作的优先级(root.callbackPriority)比拟,来决定是否有必要勾销曾经有的工作。

所以在调度工作的时候,工作优先级是不可或缺的一个重要角色。

工作优先级

工作自身是由更新产生的,因而工作优先级实质上是和 update 的优先级,即 update.lane 无关(只是无关,不肯定是由它而来)。得出的工作优先级属于 lanePriority,它不是 update 的 lane,而且与 scheduler 外部的调度优先级是两个概念,React 中的优先级转化关系能够看我总结过的一篇文章:React 中的优先级,咱们这里只探讨工作优先级的生成过程。

调度筹备 的最初提到过,update.lane 会被放入 root.pendingLanes,随后会获取 root.pendingLanes 中最优先级的那些 lanes 作为 renderLanes。工作优先级的生成就产生在计算 renderLanes 的阶段, 工作优先级其实就是 renderLanes 对应的 lanePriority。因为 renderLanes 是本次更新的优先级基准,所以它对应的 lanePriority 被作为工作优先级来掂量本次更新工作的优先级权重理所应当。

root.pendingLanes,蕴含了以后 fiber 树中所有待处理的 update 的 lane。

工作优先级有三类:

  • 同步优先级:React 传统的同步渲染模式产生的更新工作所持有的优先级
  • 同步批量优先级:同步模式到 concurrent 模式过渡模式:blocking 模式(介绍)产生的更新工作所持有的优先级
  • concurrent 模式下的优先级:concurrent 模式产生的更新持有的优先级

最右面的两个 lane 别离为同步优先级和同步批量优先级,剩下右边的 lane 简直所有都和 concurrent 模式无关。

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;

concurrent 模式下的 lanes:/*                               */ 0b1111111111111111111111111111100;

计算 renderLanes 的函数是 getNextLanes,生成工作优先级的函数是 getHighestPriorityLanes

工作优先级决定着工作在 React 中被如何调度,而由工作优先级转化成的任务调度优先级(下面给出的 scheduler 的 task 构造中的 priorityLevel),
决定着 Scheduler 何时去解决这个工作。

任务调度协调 – ensureRootIsScheduled

目前为止咱们理解了工作和工作优先级的实质,上面正式进入工作的调度过程。React 这边对工作的调度实质上其实是以工作优先级为基准,去操作多个或单个工作。

多个工作的状况,绝对于新工作,会对现有工作进行 或复用,或勾销 的操作,单个工作的状况,对工作进行 或同步,或异步,或批量同步(临时不须要关注) 的调度决策,
这种行为能够看成是一种任务调度协调机制,这种协调通过 ensureRootIsScheduled 去实现。

让咱们看一看 ensureRootIsScheduled 函数做的事件,先是筹备本次任务调度协调所须要的 lanes 和工作优先级,而后判断是否真的须要调度

  • 获取 root.callbackNode,即旧工作
  • 查看工作是否过期,将过期工作放入 root.expiredLanes,目标是让过期工作可能以同步优先级去进入调度(立刻执行)
  • 获取 renderLanes(优先从 root.expiredLanes 获取),如果 renderLanes 是空的,阐明不须要调度,间接 return 掉
  • 获取本次工作,即新工作的优先级:newCallbackPriority

接下来是协调任务调度的过程:

  • 首先判断是否有必要发动一次新调度,办法是通过比拟新工作的优先级和旧工作的优先级是否相等:

    • 相等,则阐明无需再次发动一次调度,间接复用旧工作即可,让旧工作在解决更新的时候顺便把新工作给做了。
    • 不相等,则阐明新工作的优先级肯定高于旧工作,这种状况就是 高优先级工作插队,须要把旧工作勾销掉。
  • 真正发动调度,看新工作的工作优先级:

    • 同步优先级:调用 scheduleSyncCallback 去同步执行工作。
    • 同步批量执行:调用 scheduleCallback 将工作以立刻执行的优先级去退出调度。
    • 属于 concurrent 模式的优先级:调用 scheduleCallback 将工作以下面获取到的新工作优先级去退出调度。

这里有两点须要阐明:

  1. 为什么新旧工作的优先级如果不相等,那么新工作的优先级肯定高于旧工作?

这是因为每次调度去获取工作优先级的时候,都只获取 root.pendingLanes 中最紧急的那局部 lanes 对应的优先级,低优先级的 update 持有的 lane 对应的优先级是无奈被获取到的。通过这种方法,能够将来自同一事件中的多个更新收敛到一个工作中去执行,话中有话就是同一个事件触发的屡次更新的优先级是一样的,没必要发动屡次任务调度。例如在一个事件中屡次调用 setState:

class Demo extends React.Component {
  state = {count: 0}

  onClick = () => {this.setState({ count: 1})
    this.setState({count: 2})
  }

  render() {return <button onClick={onClick}>{this.state.count}</button>
  }
}

页面上会间接显示出 2,尽管 onClick 事件调用了两次 setState,但只会引起一次调度,设置 count 为 2 的那次调度被因为优先级与设置 count 为 1 的那次工作的优先级雷同,
所以没有去再次发动调度,而是复用了已有工作。这是 React17 对于屡次 setState 优化实现的扭转,之前是通过 batchingUpdate 这种机制实现的。

  1. 三种工作优先级的调度模式有何区别,行为表现上如何?
  • 同步优先级:传统的 React 同步渲染模式和过期工作的调度。通过 React 提供的 scheduleSyncCallback 函数将工作函数 performSyncWorkOnRoot 退出到 React 本人的同步队列(syncQueue)中,之后以 ImmediateSchedulerPriority 的优先级将循环执行 syncQueue 的函数退出到 scheduler 中,目标是让工作在下一次事件循环中被执行掉。然而因为 React 的管制,这种模式下的工夫片会在工作都执行完之后再去查看,体现为没有工夫片。
  • 同步批量执行:同步渲染模式到 concurrent 渲染模式的过渡模式 blocking 模式,会将工作函数 performSyncWorkOnRoot 以 ImmediateSchedulerPriority 的优先级退出到 scheduler 中,也是让工作在下一次事件循环中被执行掉,也不会有工夫片的体现。
  • 属于 concurrent 模式的优先级:将工作函数 performConcurrentWorkOnRoot 以工作本人的优先级退出到 scheduler 中,scheduler 外部的会通过这个优先级管制该工作在 scheduler 外部工作队列中的排序,从而决定工作适合被执行,而且工作真正执行时会有工夫片的体现,能够施展出 scheduler 异步可中断调度的真正威力。

要留神一点,用来做新旧工作比拟的优先级与这里将工作退出到 scheduler 中传入的优先级不是一个,后者可由前者通过 lanePriorityToSchedulerPriority 转化而来。

通过以上的剖析,置信大家曾经对 ensureRootIsScheduled 的运行机制比拟清晰了,当初让咱们看一下它的实现:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 获取旧工作
  const existingCallbackNode = root.callbackNode;

  // 记录工作的过期工夫,查看是否有过期工作,有则立刻将它放到 root.expiredLanes,// 便于接下来将这个工作以同步模式立刻调度
  markStarvedLanesAsExpired(root, currentTime);

  // 获取 renderLanes
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  // 获取 renderLanes 对应的工作优先级
  const newCallbackPriority = returnNextLanesPriority();

  if (nextLanes === NoLanes) {
    // 如果渲染优先级为空,则不须要调度
    if (existingCallbackNode !== null) {cancelCallback(existingCallbackNode);
      root.callbackNode = null;
      root.callbackPriority = NoLanePriority;
    }
    return;
  }

  // 如果存在旧工作,那么看一下是否复用
  if (existingCallbackNode !== null) {

    // 获取旧工作的优先级
    const existingCallbackPriority = root.callbackPriority;

    // 如果新旧工作的优先级雷同,则无需调度
    if (existingCallbackPriority === newCallbackPriority) {return;}
    // 代码执行到这里阐明新工作的优先级高于旧工作的优先级
    // 勾销掉旧工作,实现高优先级工作插队
    cancelCallback(existingCallbackNode);
  }

  // 调度一个新工作
  let newCallbackNode;
  if (newCallbackPriority === SyncLanePriority) {

    // 若新工作的优先级为同步优先级,则同步调度,传统的同步渲染和过期工作会走这里
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root),
    );
  } else if (newCallbackPriority === SyncBatchedLanePriority) {

    // 同步模式到 concurrent 模式的过渡模式:blocking 模式会走这里
    newCallbackNode = scheduleCallback(
      ImmediateSchedulerPriority,
      performSyncWorkOnRoot.bind(null, root),
    );
  } else {
    // concurrent 模式的渲染会走这里

    // 依据工作优先级获取 Scheduler 的调度优先级
    const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority,);

    // 计算出调度优先级之后,开始让 Scheduler 调度 React 的更新工作
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  // 更新 root 上的工作优先级和工作,以便下次发动调度时候能够获取到
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

ensureRootIsScheduled实际上是在任务调度层面整合了高优先级工作的插队和工作饥饿问题的要害逻辑,这只是宏观层面的决策,决策背地的起因是 React 解决更新时
对于不同优先级的 update 的取舍以及对 root.pendingLanes 的标记操作,这须要咱们下沉到执行更新工作的过程中。

解决更新

一旦有更新产生,update 对象就会被放入 updateQueue 并挂载到 fiber 节点上。构建 fiber 树时,会带着 renderLanes 去解决 updateQueue,在 beginWork 阶段,对于类组件
会调用 processUpdateQueue 函数,一一解决这个链表上的每个 update 对象,计算新的状态,一旦 update 持有的优先级不够,那么就会跳过这个 update 的解决,并把这个被跳过的 update 的 lane 放到 fiber.lanes 中,好在 completeWork 阶段收集起来。

循环 updateQueue 去计算状态的过程实际上较为简单,因为低优先级 update 会被跳过并且会重做,所以这波及到最终状态对立的问题,对于这一过程的原理解读在我的这篇文章里:扒一扒 React 计算状态的原理,在本篇文章中只关注优先级相干的局部。

对于优先级的局部比拟好了解,就是只解决优先级足够的 update,跳过那些优先级有余的 update,并且将这些 update 的 lane 放到 fiber.lanes 中。咱们间接来看一下实现:

function processUpdateQueue<State>(
  workInProgress: Fiber,
  props: any,
  instance: any,
  renderLanes: Lanes,
): void {

  ...

  if (firstBaseUpdate !== null) {
    let update = firstBaseUpdate;
    do {
      const updateLane = update.lane;
      // isSubsetOfLanes 函数的意义是,判断以后更新的优先级(updateLane)// 是否在渲染优先级(renderLanes)中如果不在,那么就阐明优先级有余
      if (!isSubsetOfLanes(renderLanes, updateLane)) {

        ...

        /*
        *
        * newLanes 会在最初被赋值到 workInProgress.lanes 上,而它又最终
        * 会被收集到 root.pendingLanes。*
        * 再次更新时会从 root 上的 pendingLanes 中找出应该在本次中更新的优先
        * 级(renderLanes),renderLanes 含有本次跳过的优先级,再次进入,* processUpdateQueue wip 的优先级符合要求,被更新掉,低优先级工作
        * 因而被重做
        * */
        newLanes = mergeLanes(newLanes, updateLane);
      } else {

        // 优先级足够,去计算 state
        ...

      }
    } while (true);

    // 将 newLanes 赋值给 workInProgress.lanes,// 就是将被跳过的 update 的 lane 放到 fiber.lanes
    workInProgress.lanes = newLanes;

  }
}

只解决优先级足够的 update 是让高优先级工作被执行掉的最实质起因,在循环了一次 updateQueue 之后,那些被跳过的 update 的 lane 又被放入了 fiber.lanes,当初,只须要将它放到 root.pendingLanes 中,就能示意在本轮更新后,依然有工作未被解决,从而实现低优先级工作被从新调度。所以接下来的过程就是 fiber 节点的实现阶段:completeWork 阶段去收集这些 lanes。

收集未被解决的 lane

在 completeUnitOfWork 的时候,fiber.lanes 和 childLanes 被一层一层收集到父级 fiber 的 childLanes 中,该过程产生在 completeUnitOfWork 函数中调用的resetChildLanes,它循环 fiber 节点的子树,将子节点及其兄弟节点中的 lanes 和 childLanes 收集到以后正在 complete 阶段的 fiber 节点上的 childLanes。

假如第 3 层中的 <List/><Table/>组件都别离有 update 因为优先级不够而被跳过,那么在它们父级的 div fiber 节点 completeUnitOfWork 的时候,会调用 resetChildLanes
把它俩的 lanes 收集到 div fiber.childLanes 中,最终把所有的 lanes 收集到 root.pendingLanes.

                                    root(pendingLanes: 0b01110)|
  1                                  App
                                     |
                                     |
  2 compeleteUnitOfWork-----------> div(childLanes: 0b01110)/
                                    /
  3                              <List/> ---------> <Table/> --------> p(lanes: 0b00010)(lanes: 0b00100)(childLanes: 0b01000)/
                                 /                   /
                                /                   /
  4                            p                   ul
                                                  /
                                                 /
                                                li ------> li

在每一次往上循环的时候,都会调用 resetChildLanes,目标是将 fiber.childLanes 层层收集。

function completeUnitOfWork(unitOfWork: Fiber): void {
  // 曾经完结 beginWork 阶段的 fiber 节点被称为 completedWork
  let completedWork = unitOfWork;

  do {
    // 向上始终循环到 root 的过程
    ...

    // fiber 节点的.flags 上没有 Incomplete,阐明是失常实现了工作
    if ((completedWork.flags & Incomplete) === NoFlags) {

      ...
      // 调用 resetChildLanes 去收集 lanes
      resetChildLanes(completedWork);

      ...

    } else {/*...*/}

    ...

  } while (completedWork !== null);

  ...

}

resetChildLanes 中只收集以后正在 complete 的 fiber 节点的子节点和兄弟节点的 lanes 以及 childLanes:

function resetChildLanes(completedWork: Fiber) {

  ...

  let newChildLanes = NoLanes;

  if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {// profile 相干,无需关注} else {
    // 循环子节点和兄弟节点,收集 lanes
    let child = completedWork.child;
    while (child !== null) {
      // 收集过程
      newChildLanes = mergeLanes(
        newChildLanes,
        mergeLanes(child.lanes, child.childLanes),
      );
      child = child.sibling;
    }
  }
  // 将收集到的 lanes 放到该 fiber 节点的 childLanes 中
  completedWork.childLanes = newChildLanes;
}

最初将这些收集到的 childLanes 放到 root.pendingLanes 的过程,是产生在本次更新的 commit 阶段中,因为 render 阶段的渲染优先级来自 root.pendingLanes,不能随便地批改它。所以要在 render 阶段之后的 commit 阶段去批改。咱们看一下 commitRootImpl 中这个过程的实现:

function commitRootImpl(root, renderPriorityLevel) {

  // 将收集到的 childLanes,连同 root 本人的 lanes,一并赋值给 remainingLanes
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // markRootFinished 中会将 remainingLanes 赋值给 remainingLanes
  markRootFinished(root, remainingLanes);

  ...

}

从新发动调度

至此,咱们将低优先级工作的 lane 从新收集到了 root.pendingLanes 中,这时只须要再发动一次调度就能够了,通过在 commit 阶段再次调用 ensureRootIsScheduled 去实现,这样就又会走一遍调度的流程,低优先级工作被执行。

function commitRootImpl(root, renderPriorityLevel) {

  // 将收集到的 childLanes,连同 root 本人的 lanes,一并赋值给 remainingLanes
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // markRootFinished 中会将 remainingLanes 赋值给 remainingLanes
  markRootFinished(root, remainingLanes);

  ...

  // 在每次所有更新实现的时候都会调用这个 ensureRootIsScheduled
  // 以保障 root 上任何的 pendingLanes 都能被解决
  ensureRootIsScheduled(root, now());

}

总结

高优先级工作插队,低优先级工作重做的整个过程共有四个关键点:

  • ensureRootIsScheduled 勾销已有的低优先级更新工作,从新调度一个工作去做高优先级更新,并以 root.pendingLanes 中最重要的那局部 lanes 作为渲染优先级
  • 执行更新工作时跳过 updateQueue 中的低优先级 update,并将它的 lane 标记到 fiber.lanes 中。
  • fiber 节点的 complete 阶段收集 fiber.lanes 到父级 fiber 的 childLanes,始终到 root。
  • commit 阶段将所有 root.childLanes 连同 root.lanes 一并赋值给 root.pendingLanes。
  • commit 阶段的最初从新发动调度。

整个流程始终以高优先级工作为重,顾全大局,最可能体现 React 晋升用户体验的信心。

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


退出移动版