Skip to content

任务调度

在 React 中,触发的每一个更新不是去修改对应的组件或者 DOM 然后重新渲染就行,而是根据触发对象所处上下文环境生成对应的 Update 任务,再根据当前 Render 节点的不同选择不同的存储位置, 如在渲染阶段的更新,那么就可以直接存储到 fiber.updateQueue 链表中,等到 Commit 阶段直接处理了,如果不在渲染阶段的更新,那么就需要等待下一个调度过程处理,所以就将其存放到一个全局的数组中concurrentUpdatesQueue等到下一次调度处理。下面触发一次调度检查,通过Promise.then, setTimeout等调度器的调度方法,在合适的实际去执行对应的任务,这就是 React 中的任务调度。

这一部分就是说明,React 中任务的来源、怎么转化成对应的任务,怎么选择合适的时机去执行任务。

1. 更新来源

  1. Root 初次渲染触发的更新
  2. Class 组件 setState 的触发
  3. 函数组件 setState 触发的更新

1.1. render() 触发

这是我们一个项目或者子应用的入口,这时候整个 DOM 树,渲染树啥的都没有生成。其流程如下:

js
/**
 * 提供用户  createRoot 的 render 方法
 */
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
  function (children: ReactNodeList): void {
    updateContainer(children, root, null, null);
  };

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function
): Lane {
  const update = createUpdate(eventTime, lane);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = { element };
  // 将当前更新的组件 存放到 concurrentUpdatesQueue 中 ,这样在 workLoop 中就可以获取更新队列中组件了
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    entangleTransitions(root, current, lane);
  }

  return lane;
}

其创建的 update 对象如下

js
update = {
  // 事件时间戳
  eventTime,
  // 默认事件优先级  16
  lane: DefaultEventPriority,
  // UpdateState 方式去处理更新  - 0
  tag: UpdateState,
  // 负载  payload 是一个对象  { element }
  payload: null,
  callback: null,
  next: null,
};

其他调度更新的方式就比较统一,都是通过 enqueueUpdate(current, update, lane) 去将 update 存放到 concurrentUpdatesQueue 中,然后在 workLoop 中就可以获取更新队列中组件了,

1.2. Class 组件 setState 的触发

对于 Class 组件 state 的更新,其主要是通过 this.setState() 方法去触发的,但是其内部部分 Class 生命周期函数也会触发 state 的更新,如 constructorgetDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidUpdate 等。

下面我们先看 setState

1.2.1. setState 触发的更新

js
/**
 * 提供 Class 组件 this.setState 函数的实现功能
 * this.updater.enqueueSetState(this, partialState, callback, 'setState');
 * instance.updater.enqueueSetState(instance, state, null);
 * @param {*} inst   - 组件实例对象
 * @param {*} payload  - 更新的状态 state 或者 函数
 * @param {*} callback  - 回调函数
 */
enqueueSetState(inst, payload, callback) {
  // 获取 fiber 对象
  const fiber = getInstance(inst);
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber);
  // 创建一个 update 对象
  const update = createUpdate(eventTime, lane);
  // 将 setState 的值赋给 payload
  update.payload = payload;
  // 赋值 callback
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }
  // 将 update 对象更新到 updateQueue.share.pending 或者 concurrentQueues 数组中
  const root = enqueueUpdate(fiber, update, lane);
  // 标准的触发一下,看看是否存在空闲时机去执行当前的更新
  if (root !== null) {
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitions(root, fiber, lane);
  }
}

其跟 render 的触发也很类似,也是通过 createUpdate 方法创建一个 update 对象,然后通过 enqueueUpdate 方法将 update 对象存放到 concurrentUpdatesQueue 中,然后在 workLoop 中就可以获取更新队列中组件了。

那么气 update 对象具体是什么样子的呢?

js
function createUpdate(eventTime: number, lane: Lane): Update<*> {
  const update: Update<*> = {
    // 事件触发时间戳
    eventTime,
    // 事件优先级
    lane,
    // UpdateState 方式去处理更新
    tag: UpdateState,
    payload: state 或者 函数,
    callback: setState 的回调函数,
    // 下一个 update 对象
    next: null,
  };
}

1.2.2. getDerivedStateFromProps、getSnapshotBeforeUpdate、componentWillMount 生成的 update

js
// 如果 state 发生了变化, 则将 state 重新赋值给 instance.state
if (oldState !== instance.state) {
  classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
}

这种类型的更新 会直接替换掉当前的 state,而不是合并的方式处理,所以其 tag 是 ReplaceState

1.2.3. forceUpdate 生成的 update

有些变量不在 state 上,当时你又想达到这个变量更新的时候,刷新 render;或者 state 里的某个变量层次太深,更新的时候没有自动触发 render。这些时候都可以手动调用 forceUpdate 自动触发 render

js
Component.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, "forceUpdate");
};
enqueueForceUpdate(inst, callback) {
  const update = createUpdate(eventTime, lane);
  // ForceUpdate 方式去处理更新
  update.tag = ForceUpdate;

  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }

  const root = enqueueUpdate(fiber, update, lane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitions(root, fiber, lane);
  }
}

强制更新类型的 update

  • 这种类型的 update 由 forceUpdate 方法触发
  • 没有附带 state, 所以也不会更新 state

调用 forceUpdate()会导致组件跳过 shouldComponentUpdate(),直接调用 render()。这将触发组件的正常生命周期方法,包括每个子组件的 shouldComponentUpdate()方法。

1.3. 函数组件 setState 触发的更新

js
/**
 * 满满的Redux风格
 * Hooks中 useState useReducer 等触发变量修改的方法
 * @param {*} fiber action的FiberNode对象
 * @param {*} queue hooks的queue对象
 * @param {*} action
 * @returns
 */
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A
) {
  const lane = requestUpdateLane(fiber);
  // 创建一个 update 对象 存放到 fiberNode.updateQueue 对象中
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  // 将 update 入队,并找到 HostRoot 节点
  const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    // 计算出一个优先级时间戳
    const eventTime = requestEventTime();
    // 调度更新
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitionUpdate(root, queue, lane);
  }
}

发现 函数式组件的触发更新对象 跟 render 和 Class 组件的触发更新对象是不一样的,其调用的 enqueueConcurrentHookUpdate(fiber, queue, update, lane),但是进入其内部,发现其核心也是调用 enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane); 方法去将 update 对象入栈,但是其 update 对象的格式是不一样的

js
const update: Update<S, A> = {
  // 事件的 lane
  lane,
  action,
  hasEagerState: false,
  eagerState: null,
  next: (null: any),
};

2. 任务入栈

当生成好对应的 update 对象之后,就需要选择一个合适的地方去保存,以便在合适的时机去执行对应的任务。

对于 Class组件函数式组件 , 其入栈的方式是不一样的。

2.1. Class 组件

对于 ClassComponentrender 阶段的 update 的入栈都是通过enqueueUpdate(fiber, update, lane)这个方法来实现的,我们先看一下这个方法的实现

js
export function enqueueUpdate<State>(
  // 当前组件的FiberNode
  fiber: Fiber,
  // 更新对象 Update
  update: Update<State>,
  // 更新的优先级
  lane: Lane
): FiberRoot | null {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return null;
  }
  //
  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

  // 两种情况
  // 1. 在渲染阶段触发组件的更新操作,
  //   会将 update 对象赋值给 updateQueue.share.pending.next , 形成一个双向链表结构
  //   将当前更新的 lane 从当前组件合并到 root节点中
  // 2. 非渲染阶段触发的更新
  //   将 update对象的值 存储到 concurrentQueue 队列中
  //   将当前更新的 lane 从当前组件合并到 root节点中

  if (isUnsafeClassRenderPhaseUpdate(fiber)) {
    // This is an unsafe render phase update. Add directly to the update
    // queue so we can process it immediately during the current render.
    const pending = sharedQueue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;

    // Update the childLanes even though we're most likely already rendering
    // this fiber. This is for backwards compatibility in the case where you
    // update a different component during render phase than the one that is
    // currently renderings (a pattern that is accompanied by a warning).
    return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
  } else {
    return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
  }
}

发现其内部有两个分支,一个是在渲染阶段触发的更新,一个是非渲染阶段触发的更新,那么这两个分支的处理逻辑又有什么不同呢?

对于非渲染阶段触发的更新,其实现是通过 enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane) 方法来实现的,我们先看一下这个方法的实现

js
export function enqueueConcurrentClassUpdate<State>(
  fiber: Fiber,
  queue: ClassQueue<State>,
  update: ClassUpdate<State>,
  lane: Lane
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  // 1. 将 update 入队,concurrentQueues 数组中
  // 2. 合并 lane 到 concurrentlyUpdatedLanes , filer.childLanes, fiber.lanes, fiber.alternate.lanes
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  // 找到根节点 HostRoot 并返回
  return getRootForUpdatedFiber(fiber);
}

发现其跟 函数式组件的流程是一样的。

那么对于渲染阶段触发的更新,其怎么处理的呢?

js

核心: 将当前更新操作封装成一个 update 对象,然后添加到 sharedQueue 队列 或者 concurrentQueue 队列中

重点:

  1. 为什么对于需要区分在不同阶段的更新操作,会有不同的处理逻辑?
  • 在渲染阶段阶段触发的更新,因为还没有进行 commit 操作,所以可以直接使用最新的一次更新作为最终的结果

所以我们在 beginWork 阶段直接依据节点本身去判断是否需要重新渲染子孙节点

  • 在非渲染阶段触发的更新,因为在这个阶段是不可以被打断,所以这个阶段触发的更新是需要被存储起来的,

等到 commit 阶段再去处理,所以我们在这个阶段是将 update 对象存储到 concurrentQueue 队列中,并 将所有生成的更新的组件的 lane 合并到祖先节点中,commit 节点处理完成, 再次进行渲染阶段的时候,依据 lane,childLanes 去寻找一个个需要更新的节点线路

2.2. Hooks 的enqueueConcurrentHookUpdate(fiber, queue, update, lane)

js
export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  // 将 update 入队,  concurrentQueues 数组中
  // 2. 合并 lane 到 concurrentlyUpdatedLanes , filer.childLanes, fiber.lanes, fiber.alternate.lanes
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  // 找到根节点 HostRoot 并返回
  return getRootForUpdatedFiber(fiber);
}

发现其只是 Class 类型的 update 入栈的一部分,那么为什么 FunctionalComponent 组件的 hooks 的 update 入栈相对比较简单呐?

2.3. 区别说明

还是回到 Class 组件的生命周期图,可以发现对于 Class 组件在 Render 阶段,其有 constructorgetDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidUpdate 等生命周期函数,在这些函数内部,是可以修改 state 的,但是如果这些 update 都是通过 入栈的方式添加到 concurrentUpdate 任务队列中,那这些更新就只能到下一次渲染阶段进行处理了,大大增加了渲染的次数。

所以对于 ClassComponent 组件,我们在执行 setState 等 update 的时候,会判断其是否处于渲染阶段(isUnsafeClassRenderPhaseUpdate(fiber)),对于处于渲染阶段的 update,将其直接存放到 updateQueue.shared.pending.next 中,这样在执行组件的 render 的时候就直接可以拿到这个 update 了,这样就可以直接更新 state 了,而不需要等到下一次渲染阶段进行处理了。

对于 FunctionComponent 组件,其从组件的 constructor 到 render 之间是不建议使用 update 的,因为这样会导致组件的重新渲染,所以对于 FunctionComponent 组件就不需要考虑渲染阶段的 update 了,直接将 update 入栈到 concurrentUpdate 任务队列中。

3. 任务调度 - 获取下一步 lane

scheduleUpdateOnFiber 调度更新

js
export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number
) {
  // 查看是否存在嵌套更新
  checkForNestedUpdates();

  // Mark that the root has a pending update.
  // 在FiberRootNode 标记存在当前lane的更新
  markRootUpdated(root, lane, eventTime);

  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    // This update was dispatched during the render phase. This is a mistake
    // if the update originates from user space (with the exception of local
    // hook updates, which are handled differently and don't reach this
    // function), but there are some internal React features that use this as
    // an implementation detail, like selective hydration.
    warnAboutRenderPhaseUpdatesInDEV(fiber);

    // Track lanes that were updated during the render phase
    workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
      workInProgressRootRenderPhaseUpdatedLanes,
      lane
    );
  } else {
    // This is a normal update, scheduled from outside the render phase. For
    // example, during an input event.
    if (enableUpdaterTracking) {
      if (isDevToolsPresent) {
        addFiberToLanesMap(root, fiber, lane);
      }
    }

    warnIfUpdatesNotWrappedWithActDEV(fiber);

    if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
      if (
        (executionContext & CommitContext) !== NoContext &&
        root === rootCommittingMutationOrLayoutEffects
      ) {
        if (fiber.mode & ProfileMode) {
          let current = fiber;
          while (current !== null) {
            if (current.tag === Profiler) {
              const { id, onNestedUpdateScheduled } = current.memoizedProps;
              if (typeof onNestedUpdateScheduled === "function") {
                onNestedUpdateScheduled(id);
              }
            }
            current = current.return;
          }
        }
      }
    }

    if (enableTransitionTracing) {
      const transition = ReactCurrentBatchConfig.transition;
      if (transition !== null) {
        if (transition.startTime === -1) {
          transition.startTime = now();
        }

        addTransitionToLanesMap(root, transition, lane);
      }
    }

    if (root === workInProgressRoot) {
      // Received an update to a tree that's in the middle of rendering. Mark
      // that there was an interleaved update work on this root. Unless the
      // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
      // phase update. In that case, we don't treat render phase updates as if
      // they were interleaved, for backwards compat reasons.
      if (
        deferRenderPhaseUpdateToNextBatch ||
        (executionContext & RenderContext) === NoContext
      ) {
        workInProgressRootInterleavedUpdatedLanes = mergeLanes(
          workInProgressRootInterleavedUpdatedLanes,
          lane
        );
      }
      if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
        // The root already suspended with a delay, which means this render
        // definitely won't finish. Since we have a new update, let's mark it as
        // suspended now, right before marking the incoming update. This has the
        // effect of interrupting the current render and switching to the update.
        // TODO: Make sure this doesn't override pings that happen while we've
        // already started rendering.
        markRootSuspended(root, workInProgressRootRenderLanes);
      }
    }

    ensureRootIsScheduled(root, eventTime);
    // 如果 不是并发模式,那么就直接执行了
    if (
      lane === SyncLane &&
      executionContext === NoContext &&
      (fiber.mode & ConcurrentMode) === NoMode &&
      // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
      !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
    ) {
      // Flush the synchronous work now, unless we're already working or inside
      // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
      // scheduleCallbackForFiber to preserve the ability to schedule a callback
      // without immediately flushing it. We only do this for user-initiated
      // updates, to preserve historical behavior of legacy mode.
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();
    }
  }
}

3.1. checkForNestedUpdates 检查是否存在嵌套更新

这个主要靠 Commit 阶段进行统计,当存在嵌套更新的时候,其提交的 root 如果是同一个,那么就会累加 nestedUpdateCount,否则就会重置 nestedUpdateCount,并且将 rootWithNestedUpdates 赋值为当前的 root

js
// 在 提交阶段 缓存commit提交的次数,从而在 enqueueUpdate 的时候可以判断是否存在循环更新
if (includesSomeLane(remainingLanes, (SyncLane: Lane))) {
  if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
    markNestedUpdateScheduled();
  }

  // 当前渲染的节点是否是上次缓存的 附带循环统计的节点
  // 如果是, 就累加, 如果不是,就重置
  if (root === rootWithNestedUpdates) {
    nestedUpdateCount++;
  } else {
    nestedUpdateCount = 0;
    rootWithNestedUpdates = root;
  }
} else {
  nestedUpdateCount = 0;
}

在 enqueueUpdate 的时候,会判断是否存在循环更新,如果存在循环更新,那么就会抛出一个错误

3.2. markRootUpdated 标记根节点存在更新

在 enqueueUpdate 函数中,会将当前触发跟新的 lane 保存到 fiber.lanes 中( fiber.lanes = mergeLanes(fiber.lanes, lane);), 并通过 getRootForUpdatedFiber找到 HostRoot 根节点, 那么这一步就是将 **当前更新的 lane ** 标记到 根节点的 待处理 lanes(root.pendingLanes), 并更新时间的有效期时间戳(root.expirationTime

js
export function markRootUpdated(
  // FiberRootNode 对象
  root: FiberRoot,
  updateLane: Lane,
  eventTime: number
) {
  // 将当前的待更新的lane 保存到 root.pendingLanes 中
  // 如 默认的更新通道为 10000 = 16  那么在 root.pendingLanes 中就会保存 10000
  root.pendingLanes |= updateLane;

  if (updateLane !== IdleLane) {
    root.suspendedLanes = NoLanes;
    root.pingedLanes = NoLanes;
  }

  const eventTimes = root.eventTimes;
  // 根据 lane 找到其对应的 index
  // 因为 lane 是非常大的,如果直接用这个就造成空洞,所以将其转换成 1 所在下标,从而减少 [xx]的大小
  const index = laneToIndex(updateLane);
  // We can always overwrite an existing timestamp because we prefer the most
  // recent event, and we assume time is monotonically increasing.
  // 在 eventTimes 中保存一个当前 lane 的下标的事件的最新的时间戳
  eventTimes[index] = eventTime;
}

3.3. ensureRootIsScheduled 确保根节点已经被调度

在 3.1 和 3.2 中,已经进行了前置的检查 和 root.pendingLanesroot.expirationTimes 的保存,那么接下来就是 进行真正的任务调度了。

在这一步中,其先将上一次进行的调度任务缓存起来 const existingCallbackNode = root.callbackNode , 然后对过期任务、任务优先级等判断计算出下一个任务进行调度,其具体如下:

js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 1. 保存已经存在的回调任务
  const existingCallbackNode = root.callbackNode;

  // Check if any lanes are being starved by other work. If so, mark them as
  // expired so we know to work on those next.
  // 2. 检查是否有过期通道需要同步处理 , 并计算出 pendingLanes中lane对应的过期时间
  markStarvedLanesAsExpired(root, currentTime);

  // Determine the next lanes to work on, and their priority.
  // 3. 获取 FiberRootNode 中 下一个需要处理的 lanes
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
  );
  // 如果没有下一个需要处理的 lanes
  if (nextLanes === NoLanes) {
    // Special case: There's nothing to work on.
    // 存在之前的任务,那么就恢复之前的任务
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    // 将待处理的任务回调 和 优先级 清空
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  // We use the highest priority lane to represent the priority of the callback.
  // 获取下一个 lanes 中最高优先级的 lane
  const newCallbackPriority = getHighestPriorityLane(nextLanes);

  // Check if there's an existing task. We may be able to reuse it.
  const existingCallbackPriority = root.callbackPriority;
  // 如果新旧任务优先级相同 那么直接返回
  if (
    existingCallbackPriority === newCallbackPriority &&
    // Special case related to `act`. If the currently scheduled task is a
    // Scheduler task, rather than an `act` task, cancel it and re-scheduled
    // on the `act` queue.
    !(
      __DEV__ &&
      ReactCurrentActQueue.current !== null &&
      existingCallbackNode !== fakeActCallbackNode
    )
  ) {
    if (__DEV__) {
      // If we're going to re-use an existing task, it needs to exist.
      // Assume that discrete update microtasks are non-cancellable and null.
      // TODO: Temporary until we confirm this warning is not fired.
      if (
        existingCallbackNode == null &&
        existingCallbackPriority !== SyncLane
      ) {
        console.error(
          "Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue."
        );
      }
    }
    // The priority hasn't changed. We can reuse the existing task. Exit.
    return;
  }

  // 如果当前已经存在执行的任务,那么就取消之前的任务
  if (existingCallbackNode != null) {
    // Cancel the existing callback. We'll schedule a new one below.
    cancelCallback(existingCallbackNode);
  }

  // Schedule a new callback.
  let newCallbackNode;
  // 如果是 同步 lane
  if (newCallbackPriority === SyncLane) {
    // Special case: Sync React callbacks are scheduled on a special
    // internal queue
    // 同步任务类型的 root
    if (root.tag === LegacyRoot) {
      if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
        ReactCurrentActQueue.didScheduleLegacyUpdate = true;
      }
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    // 是否支持微任务, 支持微任务 那就使用微任务去执行回调
    if (supportsMicrotasks) {
      // Flush the queue in a microtask.
      if (__DEV__ && ReactCurrentActQueue.current !== null) {
        // Inside `act`, use our internal `act` queue so that these get flushed
        // at the end of the current scope even when using the sync version
        // of `act`.
        ReactCurrentActQueue.current.push(flushSyncCallbacks);
      } else {
        scheduleMicrotask(() => {
          // In Safari, appending an iframe forces microtasks to run.
          // https://github.com/facebook/react/issues/22459
          // We don't support running callbacks in the middle of render
          // or commit so we need to check against that.
          if (
            (executionContext & (RenderContext | CommitContext)) ===
            NoContext
          ) {
            // Note that this would still prematurely flush the callbacks
            // if this happens outside render or commit phase (e.g. in an event).
            flushSyncCallbacks();
          }
        });
      }
    } else {
      // Flush the queue in an Immediate task.
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    newCallbackNode = null;
  } else {
    let schedulerPriorityLevel;
    // 获取lanes 中最高优先级的事件优先级
    // 将 lane 转换成 事件优先级
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
    // Schedule a new callback. This is a normal Scheduler task.
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root)
    );
  }
  // 将待处理的 任务优先级 和 任务对象 存放到 root 中
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

3.3.1. markStarvedLanesAsExpired

js
/**
 * 1. 将 root.pendingLanes 中的 lane 计算出其对应的过期时间 并存放到 root.expirationTimes 中
 * 2. 如果这个 lane 已经存在过期时间了,那么就需要判断过期时间是否在当前时间之后,之后说明这是一个饥饿车道了,那么就将其标记为过期车道
 * @param {*} root
 * @param {*} currentTime
 */
export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number
): void {
  // TODO: This gets called every time we yield. We can optimize by storing
  // the earliest expiration time on the root. Then use that to quickly bail out
  // of this function.

  const pendingLanes = root.pendingLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;
  const expirationTimes = root.expirationTimes;

  // Iterate through the pending lanes and check if we've reached their
  // expiration time. If so, we'll assume the update is being starved and mark
  // it as expired to force it to finish.
  let lanes = pendingLanes;
  while (lanes > 0) {
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;
    //  获取到当前 lane 的对应的过期时间
    const expirationTime = expirationTimes[index];
    // 1. 当前车道没有过期时
    //   - 那么就需要计算出一个过期时间并保存
    // 2. 如果当前车道已经存在过期时间了
    //   - 那么就需要判断过期时间是否在当前时间之后,之后说明这是一个饥饿车道了,那么就将其标记为过期车道
    if (expirationTime === NoTimestamp) {
      // Found a pending lane with no expiration time. If it's not suspended, or
      // if it's pinged, assume it's CPU-bound. Compute a new expiration time
      // using the current time.
      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) {
        // Assumes timestamps are monotonically increasing.
        // 计算出一个过期时间并保存
        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
      // This lane expired
      // 如果这个车道的过期时间小于等于当前时间,那么就将这个车道标记为过期车道
      root.expiredLanes |= lane;
    }
    // 将当前车道 合并到 pendingLanes 中
    lanes &= ~lane;
  }
}

主要做了两件事

3.3.1.1. 计算出 pendingLanes 中 lane 对应的过期时间并保存到 root.expirationTime 中

这样会将每一个 lane 都找到一个对应的过期时间,低优先级的 lane 其过期时间较大,高优先级 lane 的过期时间小,从而保证虽然高优先级任务优先于低优先级的,但是在一定时间内,低优先级的任务也需要被处理,而不是永远被挂起,保证事件的流畅性。

计算方法是 : expirationTimes[index] = computeExpirationTime(lane, currentTime)

根据不同 lane 的更新计算出其对应的过期时间

  • 如果同步、水合、连续性输入等优先级的 lane 那么过期时间为当前时间 + 250ms
  • 如果是默认优先级的 lane 那么过期时间为当前时间 + 5000ms
  • 如果是重试的 lane 那么过期时间为 NoTimestamp
  • 如果是空闲、离屏幕任务 的 lane 那么过期时间为 NoTimestamp

这样对于高优先级的任务(如 Input 输入等事件),其计算任务的过期时间的时候就 now() + 250ms,会较早的变成过期任务,优先级更高。

而对于空闲、离屏幕任务其过期时间为 NoTimestamp,这种事件其不存在过期时间,只有当其他任务执行完成后,才会生成对应的过期时间,并进行处理。这样就形成三挡任务过期时间

  • SyncLane 等高优先级的任务

在 update 创建的时候就会计算出其过期时间,并设置较小的过期时间, 并保存到 expirationTimes 中

  • 默认、Transition 等默认优先级的任务

在 update 创建的时候就会计算出其过期时间,并设置较大的过期时间, 并保存到 expirationTimes 中

  • 空闲、离屏幕任务等最低优先级

在 update 创建的时候,不会计算出其过期时间,而是在其他任务都执行完成后,才会生成对应的过期时间,并进行处理。

js
function computeExpirationTime(lane: Lane, currentTime: number) {
  switch (lane) {
    case SyncLane:
    case InputContinuousHydrationLane:
    case InputContinuousLane:
      return currentTime + 250;
    case DefaultHydrationLane:
    case DefaultLane:
    case TransitionHydrationLane:
    case TransitionLane1:
    case TransitionLane2:
    case TransitionLane3:
    case TransitionLane4:
    case TransitionLane5:
    case TransitionLane6:
    case TransitionLane7:
    case TransitionLane8:
    case TransitionLane9:
    case TransitionLane10:
    case TransitionLane11:
    case TransitionLane12:
    case TransitionLane13:
    case TransitionLane14:
    case TransitionLane15:
    case TransitionLane16:
      return currentTime + 5000;
    case RetryLane1:
    case RetryLane2:
    case RetryLane3:
    case RetryLane4:
    case RetryLane5:
      return NoTimestamp;
    case SelectiveHydrationLane:
    case IdleHydrationLane:
    case IdleLane:
    case OffscreenLane:
      // Anything idle priority or lower should never expire.
      return NoTimestamp;
    default:
      return NoTimestamp;
  }
}
    1. 计算出 root.pendingLanes 中过期的 lane 并保存到 root.expiredLanes 中

在处理当前 lane 对应的过期时间的时候,也会将 root.pendingLanes 中已经过期的任务从root.pendingLanes移除,并添加到 root.expiredLanes, 这样在后面虽然根据 getNextLanes 计算出下一个需要处理的 lanes, 但是因为要保证任务的流畅,所以会对于 过期的任务 优先进行处理

3.3.2. getNextLanes 计算下一个需要处理的 lanes

依据 FiberRoot 和当前正在处理的车道(wipLanes)来确定下一组需要处理的更新车道(Lanes)。

在 React 的并发模式里,更新操作被赋予不同优先级的车道,该函数会综合各类因素挑选出合适的更新车道进行处理。

  • 优先级

非空闲且没有被挂起的 lane 优先级最高 > 非空闲且被挂起的 lane > 空闲且没有被挂起的 lane > 空闲且被挂起的 lane

  • 对于 InputContinuousLane , 其优先级虽然小于 SyncLane ,但是因为其是连续性的用户输入操作,所以其在处理 SyncLane 的时候一会合并处理 。

  • 为什么返回的是 一个 lanes 而不是一个 lane , 就是因为对于 InputContinuousLane ,其会被合并处理

js
/**
 * 是依据 FiberRoot 和当前正在处理的车道(wipLanes)来确定下一组需要处理的更新车道(Lanes)。
 *  在 React 的并发模式里,更新操作被赋予不同优先级的车道,该函数会综合各类因素挑选出合适的更新车道进行处理。
 * @param {*} root
 * @param {*} wipLanes 当前正在处理的车道
 * @returns
 */
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
  // Early bailout if there's no pending work left.
  // 获取 FiberRootNode 中等待更新的 lane
  const pendingLanes = root.pendingLanes;
  // 如果没有更新的 lane 就直接返回
  if (pendingLanes === NoLanes) {
    return NoLanes;
  }

  let nextLanes = NoLanes;
  // 获取 FiberRootNode 中挂起的 lane(暂停的更新)
  const suspendedLanes = root.suspendedLanes;
  // 获取 FiberRootNode 中 待处理 的 lane
  const pingedLanes = root.pingedLanes;

  // Do not work on any idle work until all the non-idle work has finished,
  // even if the work is suspended.
  // 判断是都存在非空闲的 lane
  const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
  // 1. 如果存在非空闲的 lane
  if (nonIdlePendingLanes !== NoLanes) {
    // 2. 在判断是否存在非空闲的且没有被挂起的 lane
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
    // 如果存在 没有被挂起了 那么就作为高优先级的 lane,下一个处理
    if (nonIdleUnblockedLanes !== NoLanes) {
      // 根据二进制中1的位置来获取最高优先级的 lane (最右边的)
      nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
    } else {
      // 如果不存在被挂起的,那么就将挂起的非空闲的 lane 作为下一个优先级的 lane
      const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
      if (nonIdlePingedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
      }
    }
  } else {
    // 如果不存在非空闲的 lane,还是一样 先判断是否存在非挂起的,其优先级高于挂起的 lane
    // The only remaining work is Idle.
    const unblockedLanes = pendingLanes & ~suspendedLanes;
    if (unblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLanes(unblockedLanes);
    } else {
      if (pingedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(pingedLanes);
      }
    }
  }

  if (nextLanes === NoLanes) {
    // This should only be reachable if we're suspended
    // TODO: Consider warning in this path if a fallback timer is not scheduled.
    return NoLanes;
  }

  // If we're already in the middle of a render, switching lanes will interrupt
  // it and we'll lose our progress. We should only do this if the new lanes are
  // higher priority.
  // 如果我们已经处于渲染过程中,切换车道会中断当前渲染,并且会丢失已经完成的进度。只有当新的车道优先级更高时,我们才应该进行切换
  // 1. 如果当前正在处理的 lane 存在 且 小于待处理的 nextLane 那么才会切换车道
  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    // If we already suspended with a delay, then interrupting is fine. Don't
    // bother waiting until the root is complete.
    (wipLanes & suspendedLanes) === NoLanes
  ) {
    // 判断待处理的lane的优先级
    const nextLane = getHighestPriorityLane(nextLanes);
    // 当前正在处理的 lane 的优先级
    const wipLane = getHighestPriorityLane(wipLanes);
    if (
      // Tests whether the next lane is equal or lower priority than the wip
      // one. This works because the bits decrease in priority as you go left.
      nextLane >= wipLane ||
      // Default priority updates should not interrupt transition updates. The
      // only difference between default updates and transition updates is that
      // default updates do not support refresh transitions.
      (nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
    ) {
      // Keep working on the existing in-progress tree. Do not interrupt.
      return wipLanes;
    }
  }

  if (
    allowConcurrentByDefault &&
    (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
  ) {
    // Do nothing, use the lanes as they were assigned.
  } else if ((nextLanes & InputContinuousLane) !== NoLanes) {
    // When updates are sync by default, we entangle continuous priority updates
    // and default updates, so they render in the same batch. The only reason
    // they use separate lanes is because continuous updates should interrupt
    // transitions, but default updates should not.
    // 就是当 nextLane 是 SyncLane 且存在 InputContinuousLane 的时候,那么这时候不是只进行 SyncLane 的更新,
    // 而是将 InputContinuousLane 和 SyncLane 都进行更新
    nextLanes |= pendingLanes & DefaultLane;
  }

  const entangledLanes = root.entangledLanes;
  if (entangledLanes !== NoLanes) {
    const entanglements = root.entanglements;
    let lanes = nextLanes & entangledLanes;
    while (lanes > 0) {
      const index = pickArbitraryLaneIndex(lanes);
      const lane = 1 << index;

      nextLanes |= entanglements[index];

      lanes &= ~lane;
    }
  }

  return nextLanes;
}

3.3.3. 根据任务的类型 优先级 选择不同的调度方式

在 1、2 两个步骤中我们将当前 lane 生成任务优先级和过期时间和统计出过期的任务,并计算出下一个需要处理的 lanes, 那么接下来就是根据 nextLanes 和 root.pendingLanes 来选择对应的调度方式

  1. 没有待处理的任务

清空 root 中的任务回调 和 任务优先级,并取消之前的任务回调

js
// 如果没有下一个需要处理的 lanes
if (nextLanes === NoLanes) {
  // Special case: There's nothing to work on.
  // 存在之前的任务,那么就恢复之前的任务
  if (existingCallbackNode !== null) {
    cancelCallback(existingCallbackNode);
  }
  // 将待处理的任务回调 和 优先级 清空
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  return;
}
  1. 如果新旧任务优先级相同

这边有一个区别 不是区分的 lanes 相同,而是使用的 优先级相同, 因为 getNextLanes 获取的是一个 复合的 lanes, 而不是一个 lane,所以这边我们取出这里面优先级最高的一个 lane 进行比较

js
export function getHighestPriorityLane(lanes: Lanes): Lane {
  return lanes & -lanes;
}

从而通过新旧优先级去判断是否需要重新调度任务

js
// Check if there's an existing task. We may be able to reuse it.
const existingCallbackPriority = root.callbackPriority;
// 如果新旧任务优先级相同 那么直接返回
if (existingCallbackPriority === newCallbackPriority) {
  // The priority hasn't changed. We can reuse the existing task. Exit.
  return;
}
  1. 根据新任务的优先级类型,选择不同的调度方式

从中可以看出三种回调任务的方式

    1. SyncLane 且 不是并发模式

scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));performSyncWorkOnRoot.bind(null, root) 回调函数添加到 syncQueue 数组中。

通过 浏览器的 microtask 去在执行完成后回调 flushSyncCallbacks 方法,从而将 syncQueue 数组中的任务依次执行。

js
scheduleMicrotask(() => {
  if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
    flushSyncCallbacks();
  }
});
    1. SyncLane 且 并发模式

scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); 其流程跟 1 的方式相同,使用同步任务调度添加任务,并在下一个 微任务中执行回调 flushSyncCallbacks()

    1. 非同步任务

第一步: 将 lane 转换成 四种事件优先级;

js
// 获取lanes 中最高优先级的事件优先级
// 将 lane 转换成 事件优先级
switch (lanesToEventPriority(nextLanes)) {
  case DiscreteEventPriority:
    schedulerPriorityLevel = ImmediateSchedulerPriority;
    break;
  case ContinuousEventPriority:
    schedulerPriorityLevel = UserBlockingSchedulerPriority;
    break;
  case DefaultEventPriority:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
  case IdleEventPriority:
    schedulerPriorityLevel = IdleSchedulerPriority;
    break;
  default:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
}

第二步: 按照其对应的优先级去 scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root));

然后仔细一看,其核心只分为两种方式: scheduleCallbackscheduleSyncCallback

4. 任务调度 - 添加到任务队列

4.1. 同步任务调度

js
/**
 * 同步任务回调方式,都将任务放到 syncQueue 数组中 , 然后通过 flushSyncCallbacks 方法来调度执行
 * @param {*} callback
 */
export function scheduleSyncCallback(callback: SchedulerCallback) {
  // Push this callback into an internal queue. We'll flush these either in
  // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
  if (syncQueue === null) {
    syncQueue = [callback];
  } else {
    // Push onto existing queue. Don't need to schedule a callback because
    // we already scheduled one when we created the queue.
    syncQueue.push(callback);
  }
}

4.2. 并发任务调度

js
function scheduleCallback(priorityLevel, callback) {
  if (__DEV__) {
    // If we're currently inside an `act` scope, bypass Scheduler and push to
    // the `act` queue instead.
    const actQueue = ReactCurrentActQueue.current;
    if (actQueue !== null) {
      actQueue.push(callback);
      return fakeActCallbackNode;
    } else {
      return Scheduler_scheduleCallback(priorityLevel, callback);
    }
  } else {
    // In production, always call Scheduler. This function will be stripped out.
    return Scheduler_scheduleCallback(priorityLevel, callback);
  }
}

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间
  var currentTime = getCurrentTime();
  // 获取开始时间
  var startTime;
  if (typeof options === "object" && options !== null) {
    var delay = options.delay;
    if (typeof delay === "number" && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
  // 根据事件的优先级生成其对应的过期时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }
  // 计算出过期时间
  var expirationTime = startTime + timeout;
  // 生成任务对象
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }
  // 如果开始时间 大于当前时间, 那么说明这是一个 ImmediatePriority 的任务 需要立即处理
  if (startTime > currentTime) {
    // This is a delayed task.
    // 以开始时间作为任务的排序下标
    newTask.sortIndex = startTime;
    // 将任务对象添加到定时器队列中
    push(timerQueue, newTask);
    // 如果任务队列中没有任务,并且当前任务是定时器队列中的第一个任务 timerQueue[0], 那么就立即处理当前任务
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 将任务对象的 过期时间作为其 排序下标
    newTask.sortIndex = expirationTime;
    // 将任务对象添加到任务队列中
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    // 如果不存在正在执行的任务,且没有被调度的任务
    // 那么就需要调度一个任务
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

这一步是并发任务顺序保证的核心,其也是根据任务的 lane 生成一个对应的过期时间戳,并按照不同的级别分别存放到 timerQueue 和 taskQueue 中,这样对于那些 IMMEDIATE_PRIORITY_TIMEOUT 级别的任务会立即放到 taskQueue,并判断其中是否存在并自己更早的任务,如果没有,那就立刻 flushWork 一下,而对于那些不是立即执行的优先级任务,其计算出其对应的过期时间,存放到 timerQueue, 那么在每次调度的时候 都将这些已经过期的 timerQueue 中的任务 添加到 taskQueue 中,并通过 schedulePerformWorkUntilDeadline在合适的实际去刷新处理这些任务。

具体逻辑如下:

  • 根据当前时间 和 事件优先级对应的过期时间,计算出其对应的 过期时间戳
  • 根据过期时间戳,生成任务 堆存储对象, 其中分为两个任务堆
    • timerQueue 数据:存放的是 ImmediatePriority 优先级的任务 排序:按照 任务的开始时间 进行排序 执行:没有 taskQueue 且 当前任务开始时间最早,那么就立即发起一次调度
    • taskQueue 存放的是其他类型的任务 按照 任务的过期时间进行排序 执行:如果不存在正在执行的任务,且没有被调度的任务,那么就立即发起一次调度

5. 任务调度 - 执行任务队列

在前面的流程中,可以发现对于执行任务调度,其主要分为两种: 同步任务 和 并发任务。

对于同步任务: 其通过浏览器 microtask 的能力 ,在当前 js 执行完成后就调度执行 任务队列, 即 flushSyncCallbacks

对于并发任务: 其通过两个队列 timerQueue、taskQueue 去维护不同优先级任务的顺序,并在不存在正在执行的任务,且没有被调度的任务,那么就立即发起一次调度

5.1. 同步任务

js
/**
 * 按照先进先出的方式执行syncQueue中保存的同步任务的回调
 * @returns
 */
export function flushSyncCallbacks() {
  if (!isFlushingSyncQueue && syncQueue !== null) {
    // Prevent re-entrance.
    isFlushingSyncQueue = true;
    let i = 0;
    // 获取正在执行的任务优先级
    const previousUpdatePriority = getCurrentUpdatePriority();
    try {
      const isSync = true;
      // 同步任务保存的队列
      const queue = syncQueue;
      // TODO: Is this necessary anymore? The only user code that runs in this
      // queue is in the render or commit phases.
      // 将当前执行的任务优先级 修改成 同步任务
      setCurrentUpdatePriority(DiscreteEventPriority);
      // 按照先进先出的方式执行 同步任务的回调
      for (; i < queue.length; i++) {
        let callback = queue[i];
        do {
          callback = callback(isSync);
        } while (callback !== null);
      }
      // 清空任务队列
      syncQueue = null;
      includesLegacySyncCallbacks = false;
    } catch (error) {
      // If something throws, leave the remaining callbacks on the queue.
      // 如果错误了,那么就将执行完成的任务移除
      if (syncQueue !== null) {
        syncQueue = syncQueue.slice(i + 1);
      }
      // 重新发起执行
      // Resume flushing in the next tick
      scheduleCallback(ImmediatePriority, flushSyncCallbacks);
      throw error;
    } finally {
      // 修改任务优先级为之前的优先级
      setCurrentUpdatePriority(previousUpdatePriority);
      isFlushingSyncQueue = false;
    }
  }
  return null;
}

对于同步任务 ,其在 scheduleSyncCallback 添加任务之后 就通过 microtask 回调了一下 flushSyncCallbacks,按照先进先出的方式执行 syncQueue 中保存的同步任务的回调。流程相对简单

5.2. 并发任务

对于并发任务,在入队的时候,只有两种特殊的情况才会触发,队列任务的回调。即

  • 立即执行优先级任务: 如果任务队列中没有任务,并且当前任务是定时器队列中的第一个任务 timerQueue[0], 那么就立即处理当前任务

  • 其他任务: 如果不存在正在执行的任务,且没有被调度的任务

才会调用一下 requestHostTimeout(handleTimeout, startTime - currentTime) , 或者 requestHostCallback(flushWork) , 但是对于并发任务,其都会将当前任务及其优先级存放到 root.callbackPriority = newCallbackPriority, root.callbackNode = newCallbackNode;,这两个对象中,

其他情况都是在每一次执行 scheduleCallback的时候通过添加一个定时器进入浏览器的队列中,在浏览器回调的时候执行 performWorkUntilDeadline 函数,下面我们具体看一下这个函数

js
/**
 * 检查是否还有任务需要执行,如果存在那就执行其对应的回调函数
 *   如  requestHostCallback(flushWork); 立刻执行队列中的回调
 *   requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
 *    settimeout 在一定时间后再去 执行回调
 */
const performWorkUntilDeadline = () => {
  // 如果存在回调方法
  if (scheduledHostCallback !== null) {
    // 获取当前时间
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    // 保存当前渲染帧的开始时间
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      // 回调任务
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};

/**
 * 需要立即执行的任务,
 * 所以其直接存放到 全局变量 scheduledHostCallback上,然后在下一个 setImmediate 中立即执行
 * @param {*} callback
 */
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

其中有一个重要的 scheduledHostCallback 这是一个局部变量,在任务调度的时候就会将每一个任务刷新的时机赋给这个函数,那么在调度的时候就可以回调对应的调度方法,如

  • 对于立即执行的任务,其回调函数就是 flushWork,
  • 对于低优先级的任务,回调函数就是: 一个对应时间的定时器 requestHostTimeout(handleTimeout, startTime - currentTime);

那这样对于低优先级的任务,如 UserBlockingPriority 类型的其 timeout 就是 250ms , 那么 requestHostTimeout 就是 settimeout(handleTimeout , 250) 这样在 250ms 后就会执行 handleTimeout(这时候还不是 flushWork), 其具体逻辑如下

js
/**
 * 处理可以等待的任务的执行
 * @param {*} currentTime
 */
function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime);
  // 是否正在循环处理任务中, 如果在处理任务了,那么这个任务也要延期
  if (!isHostCallbackScheduled) {
    // 如果存在 需要立即执行的任务
    //   -- 延期到下一个队列中执行
    //  如果不存在
    //   -- 处理 timerQueue 中的任务
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      //
      requestHostCallback(flushWork);
    } else {
      // 获取第一个等待处理的任务
      const firstTimer = peek(timerQueue);
      // 将其添加到任务队列中
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

这函数主要流程如下:

  1. advanceTimers(currentTime) 将 timerQueue 中过期的任务添加到 taskQueue 中,并按照 expirationTime 进行排序
js
function advanceTimers(currentTime) {
  // Check for tasks that are no longer delayed and add them to the queue.
  // 取出一个任务
  let timer = peek(timerQueue);
  while (timer !== null) {
    // 任务没有回调 - 出栈
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
      // 判断任务的开始时间 - 如果小于当前时间才开始执行
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      // 出栈
      pop(timerQueue);
      // 将任务的排序改成其过期时间
      timer.sortIndex = timer.expirationTime;
      // 扔到任务队列中
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}
  1. 如果没有执行中的任务了,那就在下一个任务队列中执行 flushWork

  2. 如果存在执行中的任务, 那就继续创建一个定时器,在对应的时候再回调当前任务

从上面可以看出对于并发任务调度,其主要是借助于定时器 handleTimeout 和 两个任务堆栈 timerQueuetaskQueue,将需要立即执行的任务和已经过期按照过期时间点进行排序,并在下一个 mocrotask 队列中回调 flushWork, 对于不需要低优先级的任务,其创建一个对应的时间的定时器,然后在哪个时间点去回调执行,并将这个任务(过期了)从 timerQueue 中转移到 taskQueue 中,并在下一个 mocrotask 队列中回调 flushWork

6. flushWork - 时间切片和执行任务

刷新任务队列,执行队列中的任务。

在前面我们知道在一个 mocrotask 后会刷新执行 taskQueue 中已经过期的 或者 需要立即执行的任务。那这一步就是将 timerQueue 中过期的任务 和 taskQueue 中准备就绪的任务 进行执行。看这一步的核心就是了解 时间切片 是如何实现的

js
function flushWork(hasTimeRemaining, initialTime) {
  // 如果开启了性能分析功能,标记调度器已恢复运行
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // 我们下次安排工作时需要一个宿主回调。当前回调正在执行,所以标记为未调度
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // 我们之前安排了一个超时任务,但现在不再需要它了,取消它
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  // 标记正在执行任务
  isPerformingWork = true;
  // 保存当前的优先级级别
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        // 调用 workLoop 函数处理任务队列中的任务,并返回是否还有未完成的任务
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        // 如果在执行任务时发生错误
        if (currentTask !== null) {
          // 获取当前时间
          const currentTime = getCurrentTime();
          // 标记当前任务出错
          markTaskErrored(currentTask, currentTime);
          // 标记当前任务已不在队列中
          currentTask.isQueued = false;
        }
        // 重新抛出错误,以便上层处理
        throw error;
      }
    } else {
      // 生产环境代码路径,没有错误捕获
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    // 任务执行完毕,清空当前任务
    currentTask = null;
    // 恢复之前的优先级级别
    currentPriorityLevel = previousPriorityLevel;
    // 标记任务执行结束
    isPerformingWork = false;
    if (enableProfiling) {
      // 获取当前时间
      const currentTime = getCurrentTime();
      // 标记调度器已暂停
      markSchedulerSuspended(currentTime);
    }
  }
}

在进入到 渲染流程的时候(flushWork),并传入一个核心的变量 initialTime 渲染初始化时间,这个不是每一个任务的执行时间,而是当前渲染周期的开始时间,那么我们在执行每一个 task 的时候就可以通过 now() - initialTime 计算出当前是否还有剩余的时间去继续进行任务或者执行下一个 task。

performWorkUntilDeadline的时候会将当前时间赋值给 startTime 并传给 flushWork,这边比较难以理解的就是其不是直接调用 flushWork ,而是调用的scheduledHostCallback(hasTimeRemaining, currentTime) 这个方法就是在 requestTimeout 的时候通过全局变量将 flushWork 变成 scheduledHostCallback 的。

js
const performWorkUntilDeadline = () => {
  // 如果存在回调方法
  if (scheduledHostCallback !== null) {
    // 获取当前时间
    const currentTime = getCurrentTime();
    // 保存当前渲染帧的开始时间
    startTime = currentTime;
    // 回调 flushWork
    hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
  }
};

然后在循环遍历 taskQueue 的时候进行了 shouldYieldToHost()的判断,这个方法就是时间切片的核心。

其通过渐进式让步的方式去保持应用响应性的同时最大化任务吞吐量,在任务执行 getCurrentTime() - startTime小于 5ms 的时候不执行让步,防止频繁的渲染导致 js 执行过慢, 也通过进入 Commit 流程的时候,将 needsPaint 置为 true,这样虽然并发回调任务触发了,且进入 taskQueue 队列了,但是因为进入 Commit 流程 ,不能被打断,所以这些任务是无法执行的,需要等待下一个任务队列执行。 例外对于 Input 等提供用户通过 navigator.scheduling.isInputPending()的返回值去手动控制是否在 50ms 和 300ms 的时候进行打断流程

js
function shouldYieldToHost() {
  // 获取渲染执行时间长度
  const timeElapsed = getCurrentTime() - startTime;
  // 如果小于 5ms 那就不需要
  if (timeElapsed < frameInterval) {
    return false;
  }
  // 基于浏览器输入状态的决策
  if (enableIsInputPending) {
    // 需要立即绘制时让出线程 给浏览器绘制
    // 如 commit 完成后,这时候正好有任务到期触发,
    // 这时候就会因为 进入UI绘制阶段,虽然触发但是无法执行
    // 等下一次 渲染线程阶段,判断这个任务已经准备好,通过 advanceTimers 去 进入 taskQueue
    if (needsPaint) {
      // There's a pending paint (signaled by `requestPaint`). Yield now.
      return true;
    }
    // 如果在 50 ms 以内, 也会检查是否是 input 状态,这时候也会中止渲染
    if (timeElapsed < continuousInputInterval) {
      if (isInputPending !== null) {
        return isInputPending();
      }
      //  300ms 以内检查
    } else if (timeElapsed < maxInterval) {
      // Yield if there's either a pending discrete or continuous input.
      if (isInputPending !== null) {
        return isInputPending(continuousOptions);
      }
    } else {
      // We've blocked the thread for a long time. Even if there's no pending
      // input, there may be some other scheduled work that we don't know about,
      // like a network event. Yield now.
      return true;
    }
  }

  // `isInputPending` isn't available. Yield now.
  return true;
}