React 源码解读之优先级

1,606 阅读22分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

react 版本:v17.0.3

UI产生交互的根本原因是各种事件,这也就意味着事件与更新有着直接的联系。在React中,由于事件有不同的等级,这也就导致了不同事件产生的更新,它们有着不一样的优先级,因此事件的优先级是更新优先级的根源。一个更新的产生可直接导致React生成一个更新任务,最终这个任务被Scheduler调度。

在React中,事件被划分了不同的等级,其最终目的是决定任务调度的轻重缓急,从而实现React的增量渲染,预防掉帧,同时达到页面更顺滑的目的,提升用户体验。因此,React有一套从事件到调度的优先级机制。

事件优先级

事件优先级划分

React按照事件的紧急程度,将它们分成了三个等级:

  • **DiscreteEventPriority:**离散事件,如 click、keydown、focusin 等,这些事件的触发不是连续的,优先级最高

  • **ContinuousEventPriority:**阻塞事件,如 drag、mousemove、scroll 等,这些事件的特点是连续触发,会阻塞渲染,优先级为适中

  • **DefaultEventPriority:**如 load、animation 等事件,优先级最低

事件优先级的具体划分如下:

// packages/react-dom/src/events/ReactDOMEventListener.js

export function getEventPriority(domEventName: DOMEventName): * {
  switch (domEventName) {
    // Used by SimpleEventPlugin:
    case 'cancel':
    case 'click':
    case 'close':
    case 'contextmenu':
    case 'copy':
    case 'cut':
    case 'auxclick':
    case 'dblclick':
    case 'dragend':
    case 'dragstart':
    case 'drop':
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'invalid':
    case 'keydown':
    case 'keypress':
    case 'keyup':
    case 'mousedown':
    case 'mouseup':
    case 'paste':
    case 'pause':
    case 'play':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointerup':
    case 'ratechange':
    case 'reset':
    case 'resize':
    case 'seeked':
    case 'submit':
    case 'touchcancel':
    case 'touchend':
    case 'touchstart':
    case 'volumechange':
    // Used by polyfills:
    // eslint-disable-next-line no-fallthrough
    case 'change':
    case 'selectionchange':
    case 'textInput':
    case 'compositionstart':
    case 'compositionend':
    case 'compositionupdate':
    // Only enableCreateEventHandleAPI:
    // eslint-disable-next-line no-fallthrough
    case 'beforeblur':
    case 'afterblur':
    // Not used by React but could be by user code:
    // eslint-disable-next-line no-fallthrough
    case 'beforeinput':
    case 'blur':
    case 'fullscreenchange':
    case 'focus':
    case 'hashchange':
    case 'popstate':
    case 'select':
    case 'selectstart':
      return DiscreteEventPriority;
    case 'drag':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'mousemove':
    case 'mouseout':
    case 'mouseover':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'scroll':
    case 'toggle':
    case 'touchmove':
    case 'wheel':
    // Not used by React but could be by user code:
    // eslint-disable-next-line no-fallthrough
    case 'mouseenter':
    case 'mouseleave':
    case 'pointerenter':
    case 'pointerleave':
      return ContinuousEventPriority;
    case 'message': {
      // We might be in the Scheduler callback.
      // Eventually this mechanism will be replaced by a check
      // of the current priority on the native scheduler.
      const schedulerPriority = getCurrentSchedulerPriorityLevel();
      switch (schedulerPriority) {
        case ImmediateSchedulerPriority:
          return DiscreteEventPriority;
        case UserBlockingSchedulerPriority:
          return ContinuousEventPriority;
        case NormalSchedulerPriority:
        case LowSchedulerPriority:
          // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
          return DefaultEventPriority;
        case IdleSchedulerPriority:
          return IdleEventPriority;
        default:
          return DefaultEventPriority;
      }
    }
    default:
      return DefaultEventPriority;
  }
}

事件优先级绑定

在「React 源码解读之合成事件」一文中,详细解析了事件的绑定。在根DOM容器(div#root) 上注册事件时,会根据事件名称,调用getEventPriority函数获取当前事件的优先级,然后根据当前事件的优先级,创建不同优先级的事件监听器listener,最终将该监听器绑定到 root 上去。

事件绑定到 root 上的一个简单流程如下 (具体流程可阅读「React 源码解读之合成事件」) :

1、首先是调用 createEventListenerWrapperWithPriority函数构造事件监听器:

// packages/react-dom/src/events/DOMPluginEventSystem.js

// 1、构造事件监听器
let listener = createEventListenerWrapperWithPriority(
  targetContainer,
  domEventName,
  eventSystemFlags,
);

2、然后在 createEventListenerWrapperWithPriority 函数中调用getEventPriority函数获取事件的优先级:

// 根据事件名称获取事件的优先级
const eventPriority = getEventPriority(domEventName);

3、接着根据事件优先级创建对应优先级的事件监听器

let listenerWrapper;
// 根据事件优先级返回对应的事件监听函数
switch (eventPriority) {
  case DiscreteEventPriority: // 事件优先级最高
    listenerWrapper = dispatchDiscreteEvent;
    break;
  case ContinuousEventPriority: // 事件优先级适中
    listenerWrapper = dispatchContinuousEvent;
    break;
  case DefaultEventPriority: // 事件优先级最低
  default:
    listenerWrapper = dispatchEvent;
    break;
}

4、最后返回当前事件的监听器,并最终调用原生事件监听函数addEventListener将监听器注册到root上:

// 返回当前事件的事件监听函数
return listenerWrapper.bind(
  null,
  domEventName,
  eventSystemFlags,
  targetContainer,
);

更新优先级

在「React Hooks 源码解读之 useState」一文中,我们介绍过 hook 对象的创建。它是一个链表,在每个hook上都有一个queue对象,它也是一个链表,存储着当前hook对象的所有update。这些 update,都是通过 createUpdate 构造函数创建的。源码如下:

// packages/react-reconciler/src/ReactUpdateQueue.new.js
export function createUpdate(eventTime: number, lane: Lane): Update<*> {
  const update: Update<*> = {
    eventTime,
    lane,

    tag: UpdateState,
    payload: null,
    callback: null,

    next: null,
  };
  return update;
}

在 update 对象中,它的 lane 属性代表它的优先级,即更新优先级。

创建update的 4 中情况

在 React 体系中,有 4 种情况会创建 update 对象。

1、应用初始化

React应用的启动模式无论是legacy模式还是 Concurrent模式,都会调用 updateContainer 函数来更新 root。在 updateContainer函数中,会调用 createUpdate函数创建一个 update对象:

// packages/react-reconciler/src/ReactFiberReconciler.new.js
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // 删除了 Dev部分的代码

  // container 的current属性保存了更新过程中的一棵fiber树,对应着屏幕上已经渲染好的内容
  // 获取更新过程中的 current树
  const current = container.current;
  // 获取当前时间
  const eventTime = requestEventTime();
  // 创建一个优先级变量(lane模型,通常称为车道模型)
  const lane = requestUpdateLane(current);

  // 删除了部分代码

  // 删除了 Dev部分的代码

  // 新建一个 update
  const update = createUpdate(eventTime, lane);

  // 删除了部分代码

  // 将新建的 update 添加到 update链表中
  enqueueUpdate(current, update, lane);
  const root = scheduleUpdateOnFiber(current, lane, eventTime);
  if (root !== null) {
    entangleTransitions(root, current, lane);
  }

  return lane;
}

2、发起组件更新

当我们在class组件中调用了 setState,本质上调用的是enqueueSetState,在enqueueSetState中,会调用 createUpdate函数创建一个 update对象:

// packages/react-reconciler/src/ReactFiberClassComponent.new.js
const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const eventTime = requestEventTime();
    const lane = requestUpdateLane(fiber);

    // 创建update对象
    const update = createUpdate(eventTime, lane);
   
    // ...

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

  },
  
  // ...
  
};

3、context的更新

在更新 context 前,会先从 ContextProvider 类型的节点开始,遍历 fiber 链表,从fiber上的context依赖列表寻找匹配的 consumers,然后调用createUpdate函数创建一个 update对象:

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberNewContext.new.js#L189

function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {

  // ...
  
  let fiber = workInProgress.child;
  if (fiber !== null) {
    // Set the return pointer of the child to the work-in-progress fiber.
    fiber.return = workInProgress;
  }
  while (fiber !== null) {
    let nextFiber;

    // Visit this fiber.
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // Check if the context matches.
        if (dependency.context === context) {
          // Match! Schedule an update on this fiber.
          if (fiber.tag === ClassComponent) {
            // Schedule a force update on the work-in-progress.
            const lane = pickArbitraryLane(renderLanes);
            
            // 调用createUpdate函数创建一个 update对象
            const update = createUpdate(NoTimestamp, lane);
            update.tag = ForceUpdate;

            // ...
            
          }

          // ...
        }
        dependency = dependency.next;
      }
    }
    
    fiber = nextFiber;
  }
}

4、在执行组件生命周期函数时发生错误

在组件的整个生命周期过程中,如果在执行某个生命周期函数时发生了错误,也会调用 createUpdate函数创建一个update对象:

// packages/react-reconciler/src/ReactFiberThrow.new.js
function createRootErrorUpdate(
  fiber: Fiber,
  errorInfo: CapturedValue<mixed>,
  lane: Lane,
): Update<mixed> {
  // 创建 update 对象
  const update = createUpdate(NoTimestamp, lane);
  // Unmount the root by rendering null.
  update.tag = CaptureUpdate;
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = {element: null};
  const error = errorInfo.value;
  update.callback = () => {
    onUncaughtError(error);
    logCapturedError(fiber, errorInfo);
  };
  return update;
}

function createClassErrorUpdate(
  fiber: Fiber,
  errorInfo: CapturedValue<mixed>,
  lane: Lane,
): Update<mixed> {
    
  // 创建 update 对象
  const update = createUpdate(NoTimestamp, lane);
  update.tag = CaptureUpdate;
 
  // ...

  return update;
}

在上述4种情况中,只有「应用初始化」和「发起组件更新」这两种情况创建的update.lane的逻辑是一样的,都是根据当前时间,创建一个update优先级。其余的两种情况,update优先级是从外部传入的。

这里,我们重点关注「应用初始化」和「发起组件更新」这两种情况创建的update.lane的逻辑。在创建 update对象之前,它们都是调用了 requestUpdateLane函数来创建update的优先级。

获取update优先级

// react-reconciler/src/ReactFiberWorkLoop.new.js

export function requestUpdateLane(fiber: Fiber): Lane {
  // Special cases
  // fiber 节点上的 mode 属性值来源于 HostFiberRoot 对象的mode
  // 标记了属于哪种启动模式(legacy模式、Concurrent模式)
  const mode = fiber.mode;
  // 启动不是 Concurrent 模式,表示是使用同步优先级做渲染
  if ((mode & ConcurrentMode) === NoMode) {
    // 启动模式为 legacy 模式
    // 返回同步优先级
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    // This is a render phase update. These are not officially supported. The
    // old behavior is to give this the same "thread" (lanes) as
    // whatever is currently rendering. So if you call `setState` on a component
    // that happens later in the same render, it will flush. Ideally, we want to
    // remove the special case and treat them as if they came from an
    // interleaved event. Regardless, this pattern is not officially supported.
    // This behavior is only a fallback. The flag only exists until we can roll
    // out the setState warning, since existing code might accidentally rely on
    // the current behavior.

    // 启动模式是 Concurrent 模式,返回这个正在执行任务的 lane
    // 当前新的任务会和现有的任务进行一次批量更新
    // pickArbitraryLane 函数返回的lane优先级是高优先级
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  // 过渡优先级 分配
  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {
    if (
      __DEV__ &&
      warnOnSubscriptionInsideStartTransition &&
      ReactCurrentBatchConfig._updatedFibers
    ) {
      // 根据过渡优先级往set集合_updatedFibers里添加fiber节点
      ReactCurrentBatchConfig._updatedFibers.add(fiber);
    }
    // The algorithm for assigning an update to a lane should be stable for all
    // updates at the same priority within the same event. To do this, the
    // inputs to the algorithm must be the same.
    //
    // The trick we use is to cache the first of each of these inputs within an
    // event. Then reset the cached values once we can be sure the event is
    // over. Our heuristic for that is whenever we enter a concurrent work loop.
    if (currentEventTransitionLane === NoLane) {
      // All transitions within the same event are assigned the same lane.
      // 通过过渡优先级分配规则分配过渡优先级
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }

  // 更新优先级

  // Updates originating inside certain React methods, like flushSync, have
  // their priority set by tracking it with a context variable.
  //
  // The opaque type returned by the host config is internally a lane, so we can
  // use that directly.
  // TODO: Move this type conversion to the event priority module.
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }


  // 事件优先级

  // This update originated outside React. Ask the host environment for an
  // appropriate priority, based on the type of event.
  //
  // The opaque type returned by the host config is internally a lane, so we can
  // use that directly.
  // TODO: Move this type conversion to the event priority module.
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
}

requestUpdateLane函数的作用是返回一个合适的update优先级。

1、根据启动模式来决定update的优先级

  • 启动模式为legacy模式,那么update的优先级为 SyncLane,即同步优先级。

  • 启动模式为Concurrent模式,则调用pickArbitraryLane 函数返回正在执行任务的 lane,是一个高优先级。

    const mode = fiber.mode; // 启动不是 Concurrent 模式,表示是使用同步优先级做渲染 if ((mode & ConcurrentMode) === NoMode) { // 启动模式为 legacy 模式 // 返回同步优先级 return (SyncLane: Lane); } else if ( !deferRenderPhaseUpdateToNextBatch && (executionContext & RenderContext) !== NoContext && workInProgressRootRenderLanes !== NoLanes ) { // 启动模式是 Concurrent 模式,返回这个正在执行任务的 lane // 当前新的任务会和现有的任务进行一次批量更新 // pickArbitraryLane 函数返回的lane优先级是高优先级 //workInProgressRootRenderLanes是一个全局变量,存储着当前工作中的根节点的优先级 return pickArbitraryLane(workInProgressRootRenderLanes); }

2、处于 suspense 过程中,根据过渡优先级分配规则来分配过渡优先级。

分配优先级时,会从过渡优先级的最右边开始分配,后续产生的任务则会依次向左移动一位,直到最后一个位置被分配后,后面的任务会从最右边第一个位置再开始做分配

// 过渡优先级 分配
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
  
  // ...
  
  if (currentEventTransitionLane === NoLane) {
    // All transitions within the same event are assigned the same lane.
    // 通过过渡优先级分配规则分配过渡优先级
    currentEventTransitionLane = claimNextTransitionLane();
  }
  return currentEventTransitionLane;
}

3、调用getCurrentUpdatePriority来获取当前update任务的优先级:

// 更新优先级
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
  return updateLane;
}

getCurrentUpdatePriority 源码如下:

// packages/react-reconciler/src/ReactEventPriorities.new.js


let currentUpdatePriority: EventPriority = NoLane;

export function getCurrentUpdatePriority(): EventPriority {
  return currentUpdatePriority;
}

4、调用getCurrentEventPriority,根据当前事件的类型,获取当前事件的优先级:

// 事件优先级
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;

在 「应用初始化」和「发起组件更新」这两种情况中,通过requestUpdateLane获取update优先级后,根据当前的update优先级创建一个update对象,最后通过scheduleUpdateOnFiber(current, lane, eventTime)函数,将 update.lane 带入调度阶段。

我们继续跟踪这个update优先级的去向。

update优先级的去向

在scheduleUpdateOnFiber中,update优先级会被 markUpdateLaneFromFiberToRoot 经过二进制运算后添加到fiber节点的 lanes 属性上。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  lane: Lane,
): FiberRoot | null {
  // Update the source fiber's lanes
  // 更新 lane优先级
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  let alternate = sourceFiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
  // ...
    
  // Walk the parent path to the root and update the child lanes.
  // 更新父路径上父节点的 childLanes 优先级
  let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    } else {
      /// ...
    }
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;
    return root;
  } else {
    return null;
  }
}

除了将update优先级添加到fiber节点的lanes属性上,还会通过 markRootUpdated 将update优先级添加到 root 的 pendingLanes 属性上,标记root 上有一个正在等待中的update。

// packages/react-reconciler/src/ReactFiberLane.new.js
export function markRootUpdated(
  root: FiberRoot,
  updateLane: Lane,
  eventTime: number,
) {
  root.pendingLanes |= updateLane;

  // If there are any suspended transitions, it's possible this new update
  // could unblock them. Clear the suspended lanes so that we can try rendering
  // them again.
  //
  // TODO: We really only need to unsuspend only lanes that are in the
  // `subtreeLanes` of the updated fiber, or the update lanes of the return
  // path. This would exclude suspended updates in an unrelated sibling tree,
  // since there's no way for this update to unblock it.
  //
  // We don't do this if the incoming update is idle, because we never process
  // idle updates until after all the regular updates have finished; there's no
  // way it could unblock a transition.
  if (updateLane !== IdleLane) {
    root.suspendedLanes = NoLanes;
    root.pingedLanes = NoLanes;
  }

  const eventTimes = root.eventTimes;
  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[index] = eventTime;
}

在 markRootUpdated 中添加到 root 上的update优先级,将会在任务优先级计算中由它计算出任务优先级。

在scheduleUpdateOnFiber中,update优先级还会同时被添加到全局变量 workInProgressRootUpdatedLanes 上。

if (
    deferRenderPhaseUpdateToNextBatch ||
    (executionContext & RenderContext) === NoContext
  ) {
    workInProgressRootUpdatedLanes = mergeLanes(
      workInProgressRootUpdatedLanes,
      lane,
    );
  }

任务优先级

一个带有lane优先级的update会被一个React的更新任务执行掉,任务优先级用来区分多个更新任务的紧急程度,谁的优先级高,就先处理谁。任务优先级是根据更新优先级计算而来。

我们假设当前产生了一前一后两个update,它们持有各自的更新优先级,也会被各自的更新任务执行。经过优先级计算后,如果后者的任务优先级等于前者的任务优先级,Scheduler 不会取消前者的任务调度,而是复用前者的更新任务,将两个同等优先级的更新合并到一次任务中。如果后者的任务优先级高于前者的任务优先级,那么 Scheduler 就会暂停或取消前者的任务调度,转而调度高优先级的后者。如果后者的的任务优先级低于前者的任务优先级,前者的任务调度也不会被Scheduler取消,而是在前者更新完成后,再次调用 Scheduler 对后者发起一次任务调度。

下面,我们通过代码来解释上面的结论。

1、任务优先级计算

任务优先级在任务即将调度之前去计算,其代码逻辑在ensureRootIsScheduled函数中:

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

// 使用这个函数为 root 安排任务,每个 root 只有一个任务
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  
  //...

  // 确定下一个要工作的lane车道,以及它们的优先级

  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  //...

  // We use the highest priority lane to represent the priority of the callback.
  // 获取最高优先级的lane车道,用来表示 Scheduler.scheduleCallback 返回的节点的优先级
  const newCallbackPriority = getHighestPriorityLane(nextLanes);

  // ...

}

在 ensureRootIsScheduled 中,调用 getNextLanes 计算出在本次更新中应该处理的这批lanes (nextLanes),从而确定下一个要工作的lane车道及其优先级。

任务优先级计算的原理是这样的:存储在root对象上的lanes (expiredLanes、suspendedLanes、pingedLanes等) 经过 getNextLanes 处理后,挑出那些当前需要紧急处理的 lanes,然后将这些lanes传入 getHighestPriorityLane 中,找出这些lanes的优先级,作为任务优先级。

2、复用现有的更新任务

// 使用这个函数为 root 安排任务,每个 root 只有一个任务
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  
  //...

  // 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
    )
  ) {
    
    // The priority hasn't changed. We can reuse the existing task. Exit.
    // 优先级没有改变,则重用现有的任务
    return;
  }

 // ...

}

从上面的代码可以看出,如果前后两个update的优先级是同等的,则直接return,不再执行后续的代码。也就是说,前后两个update的优先级相同时,Scheduler 不会取消前者的任务调度,而是复用前者的更新任务,将两个同等优先级的更新合并到一次任务中。

如果前后两个update的优先级不同,则会执行return后面的代码,发起新的任务调度。

调度优先级

在React体系中,只有并发任务 (concurrent task) 才会经过Scheduler调度。在 Scheduler 中,这个任务会被 unstable_scheduleCallback 进行包装,生成一个属于 Scheduler 自己的 task,这个task持有的优先级就是调度优先级。这个调度优先级由事件优先级和任务优先级计算得出。

调度优先级定义

// packages/scheduler/src/SchedulerPriorities.js
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

调度优先级计算

// 使用这个函数为 root 安排任务,每个 root 只有一个任务 
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  
  //...

  // if 逻辑处理的是同步任务,同步任务不需经过 Scheduler
  if (newCallbackPriority === SyncLane) {
   
    // ...

  } else {

    // else 逻辑处理的是并发任务,并发任务需要经过 Scheduler

    // 调度优先级等级
    let schedulerPriorityLevel;
    // 根据事件优先级决定调度优先级的等级
    switch (lanesToEventPriority(nextLanes)) {
      // DiscreteEventPriority  离散事件优先级最高,赋予 立即执行任务的优先级(最高)
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
        // ContinuousEventPriority:阻塞事件优先级为中优先级,赋予用户阻塞任务的优先级
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      // DefaultEventPriority 优先级,赋予正常的调度优先级
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      // IdleEventPriority  优先级,赋予最低的调度优先级
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        // 默认情况下是正常的调度优先级
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }

    // 根据调度优先级,调度并发任务
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  
  //...
  
}

在上面的代码中(else 语句模块),首先定义了一个调度优先级变量schedulerPriorityLevel,然后把经过getNextLanes计算出来的任务优先级传入 lanesToEventPriority(nextLanes) 中,获取nextLanes的事件优先级,再根据nextLanes的事件优先级赋予相应的调度优先级。

下面我们来看看 lanesToEventPriority 是如何获取到nextLanes所对应的事件优先级的。

// packages/react-reconciler/src/ReactEventPriorities.new.js

export function isHigherEventPriority(
  a: EventPriority,
  b: EventPriority,
): boolean {
  return a !== 0 && a < b;
}

// 任务优先级转换为事件优先级
export function lanesToEventPriority(lanes: Lanes): EventPriority {
  const lane = getHighestPriorityLane(lanes);
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}

在 lanesToEventPriority 函数中,首先调用 getHighestPriorityLane 函数获取nextLanes中最高优先级的 lane 车道,然后分别调用isHigherEventPriority 和 includesNonIdleWork函数,判断这个最高优先级的lane车道所属的事件优先级。

Scheduler 包装任务

并发任务在经过Scheduler调度时,会被Scheduler包装成属于 Scheduler 自己的 task。这部分的处理逻辑在 Scheduler 的unstable_scheduleCallback函数中。

// packages/scheduler/src/forks/Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 这个 currentTime 获取的是 performance.now() 时间戳
  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;

  // 属于 Scheduler 自己的 task
  var newTask = {
    id: taskIdCounter++,
    callback, // 这里的 callback 是 performConcurrentWorkOnRoot 函数
    priorityLevel, // 调度优先级
    startTime, // 任务开始时间
    expirationTime, // 任务过期时间
    sortIndex: -1,  // 用于task的优先级排序
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // Scheduler调度任务时传入了 delay time,startTime 是大于 currentTime 的,表示这个任务将会延迟执行
  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    // timerQueue 是一个二叉堆结构,以最小堆的形式存储 task
    // 向二叉堆中添加节点
    push(timerQueue, newTask);
    // peek 查看堆的顶点, 也就是优先级最高的`task`
    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;
     // taskQueue 是一个二叉堆结构,以最小堆的形式存储 task
    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;
}

可以看到,在 unstable_scheduleCallback 中:

1、首先获取performance.now() 时间戳,计算任务的startTIme。

// 这个 currentTime 获取的是 performance.now() 时间戳
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;
}

2、然后根据调度优先级设置相应的定时时间,由定时时间和startTime计算出任务的过期时间expirationTime。

// 根据调度优先级设置相应的定时时间
  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;

3、接着创建一个属于Scheduler自己的task,这个task上的 callback 是 performConcurrentWorkOnRoot 函数。

// 属于 Scheduler 自己的 task
var newTask = {
  id: taskIdCounter++,
  callback, // 这里的 callback 是 performConcurrentWorkOnRoot 函数
  priorityLevel, // 调度优先级
  startTime, // 任务开始时间
  expirationTime, // 任务过期时间
  sortIndex: -1,  // 在堆排序时用于比较两个task
};
if (enableProfiling) {
  newTask.isQueued = false;
}

4、然后把创建好的 task 存储在最小堆timerQueue或taskQueue中,以便可以在O(1) 的时间复杂度获取到优先级最高的 task。

// Scheduler调度任务时传入了 delay time,startTime 是大于 currentTime 的,表示这个任务将会延迟执行
  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    
    // timerQueue 是一个二叉堆结构,以最小堆的形式存储 task
    // 向二叉堆中添加节点
    push(timerQueue, newTask);
    // peek 查看堆的顶点, 也就是优先级最高的`task`
    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;
    
    // taskQueue 是一个二叉堆结构,以最小堆的形式存储 task
    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);
    }
  }

task 的存储与获取解析详见「React 算法应用之堆排序」。

转换关系

事件优先级、更新优先级、任务优先级、调度优先级,它们之间是递进的关系。它们的关系图如下:

1、事件优先级是更新优先级的根源,更新优先级通过调用 getCurrentUpdatePriority 函数,由事件优先级转换而来。

// packages/react-reconciler/src/ReactEventPriorities.new.js
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

let currentUpdatePriority: EventPriority = NoLane;

export function getCurrentUpdatePriority(): EventPriority {
  return currentUpdatePriority;
}

2、任务优先级用来区分多个更新任务的紧急程度,谁的优先级高,就先处理谁。任务优先级通过调用getHighestPriorityLanes函数,传入更新优先级,由更新优先级计算而来。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js  

function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
  switch (getHighestPriorityLane(lanes)) {
    case SyncLane:
      return SyncLane;
    case InputContinuousHydrationLane:
      return InputContinuousHydrationLane;
    case InputContinuousLane:
      return InputContinuousLane;
    case DefaultHydrationLane:
      return DefaultHydrationLane;
    case DefaultLane:
      return DefaultLane;
    case TransitionHydrationLane:
      return 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 lanes & TransitionLanes;
    case RetryLane1:
    case RetryLane2:
    case RetryLane3:
    case RetryLane4:
    case RetryLane5:
      return lanes & RetryLanes;
    case SelectiveHydrationLane:
      return SelectiveHydrationLane;
    case IdleHydrationLane:
      return IdleHydrationLane;
    case IdleLane:
      return IdleLane;
    case OffscreenLane:
      return OffscreenLane;
    default:
      if (__DEV__) {
        console.error(
          'Should have found matching lanes. This is a bug in React.',
        );
      }
      // This shouldn't be reachable, but as a fallback, return the entire bitmask.
      return lanes;
  }
}

3、调度优先级是Scheduler的优先级机制,它是通过任务优先级和事件优先级计算得出。首先由任务优先级计算出事件优先级,然后再由事件优先级计算出调度优先级。

//packages/react-reconciler/src/ReactEventPriorities.new.js
// 参数lanes 是任务优先级
// 任务优先级转换为事件优先级
export function lanesToEventPriority(lanes: Lanes): EventPriority {
  const lane = getHighestPriorityLane(lanes);
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}



// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// 调度优先级等级
let schedulerPriorityLevel;
// 根据事件优先级决定调度优先级的等级
// lanesToEventPriority(nextLanes) 返回的是事件优先级
// 这里由事件优先级转换为调度优先级
switch (lanesToEventPriority(nextLanes)) {
  // DiscreteEventPriority  离散事件优先级最高,赋予 立即执行任务的优先级(最高)
  case DiscreteEventPriority:
    schedulerPriorityLevel = ImmediateSchedulerPriority;
    break;
    // ContinuousEventPriority:阻塞事件优先级为中优先级,赋予用户阻塞任务的优先级
  case ContinuousEventPriority:
    schedulerPriorityLevel = UserBlockingSchedulerPriority;
    break;
  // DefaultEventPriority 优先级,赋予正常的调度优先级
  case DefaultEventPriority:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
  // IdleEventPriority  优先级,赋予最低的调度优先级
  case IdleEventPriority:
    schedulerPriorityLevel = IdleSchedulerPriority;
    break;
  default:
    // 默认情况下是正常的调度优先级
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
}

总结

  1. 本文介绍了React中的 4 种优先级:事件优先级更新优先级任务优先级调度优先级,它们之间是递进的关系。

  2. 事件优先级由事件本身决定,按照事件的紧急程度,划分了三个等级:DiscreteEventPriority、ContinuousEventPriority、DefaultEventPriority。

  3. 更新优先级由事件优先级转换而来,然后将其放到fiber节点的lanes属性和root的pendingLanes属性上。

  4. 任务优先级由更新优先级计算得来。存储在root对象上的lanes (expiredLanes、suspendedLanes、pingedLanes等) 上。经过 getNextLanes 和 getHighestPriorityLane 处理后,找出这些lanes的优先级,作为任务优先级。

  5. 调度优先级则是由任务优先级和事件优先级计算得出。首先由任务优先级计算出事件优先级,然后再由事件优先级计算出调度优先级。

  6. 通过优先级的灵活运用, React实现了**可中断渲染、时间切片(time slicing)、异步渲染(suspense)**等特性。

参考文档:

segmentfault.com/a/119000003…

juejin.cn/post/699313…