Published on

react-lane

Authors
  • avatar
    Name
    李丹秋
    Twitter

问题:

  1. 什么样的事件对应什么样的优先级?
  2. 上次讲的schedule流程一定会执行嘛?
  3. 批量更新如何实现?

react更新流程

在 react 中通过 class 组件或者 state hook 来改变组件的状态来触发页面的更新。每当我们调用一次 setState,react 会产生一个 update。并且 react 会根据该 update 产生一个渲染任务(也有可能复用之前的任务),这里简称任务。在这个任务中会进行 fiber 的 diff 并将最终结果 patch 到真实 dom 上。而创建的任务也并非立即执行, 它会被放入调度器队列,经过调度器调度执行。因此在实际的开发过程中,从事件产生到 dom 改变,往往会经过:

  1. 用户事件(例如点击,拖拽)产生,执行回调
  2. 回调中调用 setState 改变状态,产生 update
  3. react 创建渲染任务
  4. 调度器调度执行

在整个过程中都涉及到各种优先级:

  1. 用户事件产生: 根据不同的事件类型执行回调,并指定不同的事件优先级
  2. 状态改变,产生 update: 根据产生该 update 的事件优先级,计算 update 优先级
  3. react 创建渲染任务: 根据本次 update 以及目前尚未处理完成的工作计算 渲染优先级,并根据渲染优先级计算调度优先级
  4. 调度器调度执行: 根据挂载到 fiber 上的 update 优先级以及渲染优先级来控制需要跳过的 fiber 以及 update

Lane优先级

在更新流程中提到的的事件优先级,update 优先级以及渲染优先级本质都是 Lane 优先级。Lane 优先级的基本类型 Lane 在代码中使用仅有 1 位为 1,其他位都为 0 的 31 位 2 进制数表示。其中 1 的位置约靠近右边,则表示该 lane 优先级越高。因此一个 lane 的值越小则对应的优先级反而越高。部分优先级的定义如下,SyncLane 的优先级就高于 InputContinuousHydrationLane。

export const TotalLanes = 31;
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000010000;
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;

事件优先级

开发者在 react 回调中所接触到的事件都是 react 的中的合成事件。当某个事件触发后,react 在调用回调函数之前会设置一个事件优先级。例如点击事件属于 DiscreteEventPriority,其优先级高于拖拽事件的 ContinuousEventPriority 优先级。具体的事件类型和优先级的映射关系可以参考事件优先级。这里列出 react 中定义了以下几种事件优先:

export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

可以看出每个事件优先级都是等于一个 Lane 优先级。这里为什么不直接使用 Lane 优先级呢?我估计是为了解耦,例如以后 React 想将 Lane 优先级更换为其他优先级,那只需要更改事件优先级的定义即可。事件优先级会影响该事件回调函数中产生的 update 的优先级。

如何为React不同的事件添加不同的优先级

调用createRoot方法创建根节点后,会为root这个节点做事件委托

export function createRoot(
  container: Container,
  options?: CreateRootOptions,
): RootType {
    ...
    
  // 创建容器-Fiber根节点
  const root = createContainer(
    container,
    ConcurrentRoot,
    hydrate,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
  
  // 在root容器上添加事件监听,做事件委托
  listenToAllSupportedEvents(rootContainerElement);
}

也就是在这个时候,会对所有支持的事件做一个优先级的分类,并赋予这些事件不同的优先级

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 根据不同的事件做优先级分类
  const eventPriority = getEventPriority(domEventName);

  // 根据优先级分类,设置事件触发时的优先级
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

我们看到首先会调用getEventPriority方法,这个方法内部主要是将不同的事件区分为不同的优先级:

export function getEventPriority(domEventName: DOMEventName): * {
  switch (domEventName) {
    case 'cancel':
    case 'click':
    case 'copy':
    case 'dragend':
    case 'dragstart':
    case 'drop':
    ...
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'change':
    case 'textInput':
    case 'blur':
    case 'focus':
    case 'select':
      // 同步优先级
      return DiscreteEventPriority;
    case 'drag':
    case 'mousemove':
    case 'mouseout':
    case 'mouseover':
    case 'scroll':
    ...
    case 'touchmove':
    case 'wheel':
    case 'mouseenter':
    case 'mouseleave':
      // 连续触发优先级
      return ContinuousEventPriority;
   ...
    default:
      return DefaultEventPriority;
  }
}

从这个方法中可以很清晰的看到,React将用户点击,input框输入等都设置为同步优先级,这是因为用户在操作的时候需要立即得到反馈,如果操作完没有反馈就会给用户造成界面卡顿的感觉。 接下来会根据获取到的事件的优先级分类,设置事件触发时拥有相对应优先级的回调函数:

let listenerWrapper;
switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
}
  
function dispatchDiscreteEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {
  ...
  setCurrentUpdatePriority(DiscreteEventPriority);
}

function dispatchContinuousEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {
  ...
  setCurrentUpdatePriority(ContinuousEventPriority);
}

可以看到相对应回调函数中都调用了同一个方法setCurrentUpdatePriority,并且都设置了当前事件相对应的事件优先级的值。

Lane是如何在React中工作的

首先调用setState函数发起更新,setState内部调用了dispatchSetState函数:

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const lane = requestUpdateLane(fiber);
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

获取事件的优先级

首先获取到当前需要更新的组件的fiber对象,然后调用了requestUpdateLane函数获取到了当前事件的优先级,我们来看一下requestUpdateLane函数内部是如何获取到事件优先级的:

export function requestUpdateLane(fiber: Fiber): Lane {
  // 获取到当前渲染的模式:sync mode(同步模式) 或 concurrent mode(并发模式)
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    // 检查当前渲染模式是不是并发模式,等于NoMode表示不是,则使用同步模式渲染
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    // workInProgressRootRenderLanes是在任务执行阶段赋予的需要更新的fiber节点上的lane的值
    // 当新的更新任务产生时,workInProgressRootRenderLanes不为空,则表示有任务正在执行
    // 那么则直接返回这个正在执行的任务的lane,那么当前新的任务则会和现有的任务进行一次批量更新
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  // 检查当前事件是否是过渡优先级
  // 如果是的话,则返回一个过渡优先级
  // 过渡优先级的分配规则:
  // 产生的任务A给它分配为TransitionLanes的第一位:TransitionLane1 = 0b0000000000000000000000001000000
  // 现在又产生了任务B,那么则从A的位置向左移动一位: TransitionLane2 = 0b0000000000000000000000010000000
  // 后续产生的任务则会一次向后移动,直到移动到最后一位
  // 过渡优先级共有16位:                         TransitionLanes = 0b0000000001111111111111111000000
  // 当所有位都使用完后,则又从第一位开始赋予事件过渡优先级
  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {
    if (currentEventTransitionLane === NoLane) {
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }

  // 在react的内部事件中触发的更新事件,比如:onClick等,会在触发事件的时候为当前事件设置一个优先级,可以直接拿来使用。
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }

  // 在react的外部事件中触发的更新事件,比如:setTimeout等,会在触发事件的时候为当前事件设置一个优先级,可以直接拿来使用
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
}

非concurrent模式

首先会检查当前的渲染模式是否是concurrent模式,如果不是concurrent模式则都会使用同步优先级做渲染:

if ((mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
} 

concurrent模式

如果是,则会接着检查当前是否有任务正在执行,workInProgressRootRenderLanes是在初始化workInProgress树时,将当前执行的任务的优先级赋值给了workInProgressRootRenderLanes,如果workInProgressRootRenderLanes不为空,那么则直接返回这个正在执行的任务的lane,当前新的任务则会和现有的任务进行一次批量更新:

if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

如果上面都不是,则会判断当前事件是否是过渡优先级,如果是,则会分配过渡优先级中的一个位置。

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

当前产生了一个任务A,那么会分配过渡优先级的最右边第一个位置:

TransitionLane1 = 0b0000000000000000000000001000000

现在又产生了任务B,那么则从A的位置向左移动一位:

TransitionLane2 = 0b0000000000000000000000010000000

后续产生的任务则会依次向左移动一位,过渡优先级共有16位:

TransitionLanes = 0b0000000001111111111111111000000

当最左边的1的位置被分配后,则又从最右边第一位1的位置开始赋予事件过渡优先级。

如果不是过渡优先级的任务,则接着往下找,可以看到接下来调用了getCurrentUpdatePriority函数,记得我们最开始讲到过,当项目初次渲染的时候,会在root容器上做事件委托并将所有支持的事件做优先级分类,当事件触发时会调用setCurrentUpdatePriority函数设置当前事件的优先级。调用getCurrentUpdatePriority函数也就获取到了事件触发时设置的事件优先级。获取到的事件优先级不为空的话,则会直接返回该事件的优先级。

const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }

如果上面都没有找到事件优先级,则是会调用getCurrentEventPriority来获取React的外部事件的优先级,比如:在setTimeout中调用了setState方法:

const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;

最后将找到的事件的优先级返回。

使用事件的优先级

现在我们已经看到是如果获取到事件的优先级了,那么是如果使用Lane的呢?我们接下来看。

首先会创建一个更新对象,将事件的lane添加到更新对象上, 将需要更新的任务添加到action上:

  const lane = requestUpdateLane(fiber);

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

然后将更新对象添加到当前组件对应的fiber节点上的更新队列中:

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);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}

接着会调用scheduleUpdateOnFiber,做好调度任务前的准备,我们主要看其中几个重要的地方:

export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  markRootUpdated(root, lane, eventTime);
....
  ensureRootIsScheduled(root, eventTime);
}

首先调用了markRootUpdated函数,这个函数的作用是将当前需要更新的lane添加到fiber root的pendingLanes属性上,表示有新的更新任务需要被执行,然后将事件触发时间记录在eventTimes属性上:

export function markRootUpdated(
  root: FiberRoot,
  updateLane: Lane,
  eventTime: number,
) {
  // 将当前需要更新的lane添加到fiber root的pendingLanes属性上
  root.pendingLanes |= updateLane;

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

  // 假设updateLane为:0b000100
  // eventTimes是这种形式的:[-1, -1, -1, 44573.3452, -1, -1]
  // 用一个数组去储存eventTime,-1表示空位,非-1的位置和lane中1的位置相同
  const eventTimes = root.eventTimes;
  const index = laneToIndex(updateLane);
  eventTimes[index] = eventTime;
}

ensureRootIsScheduled是一个比较重要的函数,里面存在了高优先级任务插队和任务饥饿问题,以及批量更新的处理。那么我们来看一下该函数中是如何处理这些问题的。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;
 
  // 为当前任务根据优先级添加过期时间
  // 并检查未执行的任务中是否有任务过期,有任务过期则expiredLanes中添加该任务的lane
  // 在后续任务执行中以同步模式执行,避免饥饿问题
  markStarvedLanesAsExpired(root, currentTime);

  // 获取优先级最高的任务的优先级
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  // 如果nextLanes为空则表示没有任务需要执行,则直接中断更新
  if (nextLanes === NoLanes) {
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  // nextLanes获取的是所有任务中优先级最高任务的lane
  // 那么与当前现有的任务的优先级比较,只会有两种结果:
  // 1.与现有的任务优先级一样,那么则会中断当前新任务向下的执行,重用之前现有的任务
  // 2.新任务的优先级大于现有的任务优先级,那么则会取消现有的任务的执行,优先执行优先级高的任务

  // 与现有的任务优先级一样的情况
  if (
    existingCallbackPriority === newCallbackPriority
  ) {
    return;
  }

  // 新任务的优先级大于现有的任务优先级
  // 取消现有的任务的执行
  if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
  }

  // 开始调度任务
  // 判断新任务的优先级是否是同步优先级
  // 是则使用同步渲染模式,否则使用并发渲染模式(时间分片)
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    ...
    newCallbackNode = null;
  } else {
    ...
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

任务饥饿问题

关于任务饥饿问题的处理,主要逻辑在markStarvedLanesAsExpired函数中,它主要的作用是为当前任务根据优先级添加过期时间,并检查未执行的任务中是否有任务过期,有任务过期则在expiredLanes中添加该任务的lane,在后续该任务的执行中以同步模式执行,避免饥饿问题:

export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number,
): void {

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

  // 将要执行的任务会根据它们的优先级生成一个过期时间
  // 当某个任务过期了,则将该任务的lane添加到expiredLanes过期lanes上
  // 在后续执行任务的时候,会通过检查当前任务的lane是否存在于expiredLanes上,
  // 如果存在的话,则会将该任务以同步模式去执行,避免任务饥饿问题
  // ps: 什么饥饿问题?
  // 饥饿问题是指当执行一个任务时,不断的插入多个比该任务优先级高的任务,那么
  // 这个任务会一直得不到执行
  let lanes = pendingLanes;
  while (lanes > 0) {
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;

    // 获取当前位置上任务的过期时间,如果没有则会根据任务的优先级创建一个过期时间
    // 如果有则会判断任务是否过期,过期了则会将当前任务的lane添加到expiredLanes上
    const expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) {
      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) {
        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
      root.expiredLanes |= lane;
    }
    lanes &= ~lane;
  }
}

可以看到主要逻辑是在循环处理pendingLanes。

首先会调用pickArbitraryLaneIndex函数获取pendingLanes中最左边1的位置,例如:

lanes = 28 = 0b0000000000000000000000000011100

然后使用了:

27 = Math.clz32(lanes);

获取到了最左边1前面所有0的个数,然后计算出最左边1的位置:

const index = 31 - 27; // 4

然后使用index获取到相对应expirationTimes中的过期时间,如果过期时间为空则会根据当前优先级生成一个过期时间,优先级越高过期时间越小。然后将过期时间添加到相应的位置。

expirationTimes与eventTimes一样也是31位长度的Array,对应Lane使用31位的二进制。

 expirationTimes[index] = computeExpirationTime(lane, currentTime);

如果当前位置有过期时间,则会检查是否过期,如果过期则将当前lane添加到expiredLanes上,在后续执行该任务的时候使用同步渲染,避免任务饥饿的问题。

if (expirationTime <= currentTime) {
  root.expiredLanes |= lane;
}

接着会将当前计算完成的lane从lanes中删除,每次循环删除一个,直到lanes等于0:

 lanes &= ~lane;

这个函数就是根据当前的优先级集合把每一个优先级对应的过期时间填充进去,如果此刻有过期的优先级,将它添加到expiredLanes中,等到下一次调度的时候,会检查有没有过期的,过期的优先级会当作同步任务立即执行,防止出现饥饿问题。

任务插队

首先在介绍这个概念之前,我们要思考为什么要有任务优先级呢? 实际上任务优先级的出现也很自然,其实在这一步之前大家可以想象一下react的任务池子里面(也就是root.pendingLanes)中已经堆积了可能许许多多种更新(Update)对象了,但是因为javascipt是单线程的原因,一次性只能执行一个,因此react需要从这么多池子中选出最紧急的一个优先级来进行执行,这个就是选出来的就是任务优先级,只不过在选最取的时候不是选取的一个,而是一批优先级,我们可以从getNextLanes中看看相关的内容:

function getNextLanes(root, wipLanes) { 
    var pendingLanes = root.pendingLanes;
    if (pendingLanes === NoLanes) {
      return NoLanes;
    }
    var nextLanes = NoLanes; // 默认为noLanes
    // 如果当前的pendingLanes是一个 Idle类的优先级, 那么 nonIdlePendingLanes 就会变成 0 
    var nonIdlePendingLanes = pendingLanes & NonIdleLanes; // 非空闲优先级  0b0001111111111111111111111111111;

    if (nonIdlePendingLanes !== NoLanes) { // 说明 pendingLanes不是一个空闲优先级,属于优先级微高吧
      // 如果当前的pendingLanes不是空闲优先级,那么就会执行下面的代码 
      var nonIdleUnblockedLanes = nonIdlePendingLanes
      if (nonIdleUnblockedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
      } else {
        var nonIdlePingedLanes = nonIdlePendingLanes ;
        if (nonIdlePingedLanes !== NoLanes) {
          nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
        }
      }
    } else {
      var unblockedLanes = pendingLanes ;
      if (unblockedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(unblockedLanes);
      } 
      ...
    }

    if (nextLanes === NoLanes) {
      return NoLanes;
    } 
   
    // 当nextLanes 属于 InputContinuousLane 时
    if ((nextLanes & InputContinuousLane) !== NoLanes) { //0b0000000000000000000000000000100;
      nextLanes |= pendingLanes & DefaultLane; // 0b0000000000000000000000000010000;
    } 
    var entangledLanes = root.entangledLanes; // 只有属于transitions优先级的时候才会有用。
    if (entangledLanes !== NoLanes) {
      var entanglements = root.entanglements;
      var lanes = nextLanes & entangledLanes;

      while (lanes > 0) {
        var index = pickArbitraryLaneIndex(lanes);
        var lane = 1 << index;
        nextLanes |= entanglements[index];
        lanes &= ~lane;
      }
    }
    return nextLanes;
  }

这个是如何获取任务优先级的关键函数,为了简化代码,我删除了关于Suspense相关的优先级的逻辑,我们主要聚焦于正常情况下如何获取任务优先级,这个函数的主要逻辑是如何从pendingLanes中提取优先级,无论是事件、IO、Transition触发的更新都是非空闲的优先级,因此大部分情况下都是nonIdleUnblockedLanes,函数会通过getHighestPriorityLanes来分离出最高优先级集合。

function getHighestPriorityLanes(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 TransitionLane16:
        return lanes & TransitionLanes; // 返回一个集合

      case RetryLane1:
      ...
      case RetryLane5:
        return lanes & RetryLanes; // 返回一个集合
        
      case SelectiveHydrationLane:
        return SelectiveHydrationLane;
      case IdleHydrationLane:
        return IdleHydrationLane;
      case IdleLane:
        return IdleLane;
      case OffscreenLane:
        return OffscreenLane;

      default:
        {
          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;
    }
  }


可以看到除了Transition类的更新和RetryLane类的更新会返回一个优先级集合,其他类型的更新优先级都是返回单个优先级,这是为了更好的优化调度过程,因为Transition类的更新实际上就是一种更新。 整体的逻辑就是会从pendingLanes中获取最右侧的优先级,判断它属于哪一种,判断是返回一个Lane的集合还是单个Lane,但无论如何它一定是代表着当前最紧急的优先级。 接下来获得任务优先级后会有下面的操作

首先判断pendingLanes是否为空。根据之前的代码,每个产生的任务都会将它们各自的优先级添加到fiber root的pendingLanes的属性上,也就是说pendingLanes上保存了所有将要执行的任务的lane,如果pendingLanes为空,那么则表示任务全部执行完成,也就不需要更新了,直接跳出。

可以看到ensureRootIsScheduled中对于getNextLanes返回空的处理:

// 如果nextLanes为空则表示没有任务需要执行,则直接中断更新
if (nextLanes === NoLanes) {
    // existingCallbackNode不为空表示有任务使用了concurrent模式被scheduler调用,但是还未执行
    // nextLanes为空了则表示没有任务了,就算这个任务执行了但是也做不了任何更新,所以需要取消掉
    if (existingCallbackNode !== null) {
      // 使用cancelCallback会将任务的callback置为null
      // 在scheduler循环taskQueue时,会检查当前task的callback是否为null
      // 为null则从taskQueue中删除,不会执行
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
}

回过头继续看getNextLanes中的代码:

// 在将要处理的任务中检查是否有未闲置的任务,如果有的话则需要先执行未闲置的任务,不能执行挂起任务
  // 例如:
  // 当前pendingLanes为: 17 = 0b0000000000000000000000000010001 
  // NonIdleLanes          = 0b0001111111111111111111111111111
  // 结果为:                = 0b0000000000000000000000000010001 = 17  
  const nonIdlePendingLanes = pendingLanes & NonIdleLanes;

  //检查是或否还有未闲置且将要执行的任务
  if (nonIdlePendingLanes !== NoLanes) {
    //检查未闲置的任务中除去挂起的任务,是否还有未被阻塞的的任务,有的话则需要
    //从这些未被阻塞的任务中找出任务优先级最高的去执行
    // & ~suspendedLanes 相当于从 nonIdlePendingLanes 中删除 suspendedLanes
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
    if (nonIdleUnblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
    } else {
      // nonIdleUnblockedLanes(未闲置且未阻塞的任务)是未闲置任务中除去挂起的任务剩下来的
      // 如果nonIdleUnblockedLanes为空,那么则从剩下的,也就是挂起的任务中找到优先级最高的来执行
      const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
      if (nonIdlePingedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
      }
    }
  } else {
    // The only remaining work is Idle.
    // 剩下的任务都是闲置的
    // 找出未被阻塞的任务,然后从中找出优先级最高的执行
    const unblockedLanes = pendingLanes & ~suspendedLanes;
    if (unblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLanes(unblockedLanes);
    } else {
      // 进入到这里,表示目前的任务中已经没有了未被阻塞的任务
      // 需要从挂起的任务中找出任务优先级最高的执行
      if (pingedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(pingedLanes);
      }
    }
  }

如果pengdingLanes不为空,那么则会从pengdingLanes中取出未闲置将要处理的lanes,例如:

当前pendingLanes为:    = 0b0100000000000000000000000010001 

最左边的1的位置为闲置位置,代表了闲置任务,闲置任务优先级最低,需要处理完所有其它优先级的任务后,再处理闲置任务。

NonIdleLanes          = 0b0001111111111111111111111111111

NonIdleLanes表示了所有未闲置的1的位置,使用&符号运算(同位比较,值都为1,则结果为1,否则为0),取出未闲置的任务:

结果为:                = 0b0000000000000000000000000010001 = 17  

如果有未闲置的lanes,那么会优先找到未闲置lanes中未被阻塞的lane,如果没找到,则会从挂起的lanes中找到优先级最高的lane。 如果没有未闲置的lanes,则会从闲置的lanes中优先找未被阻塞的lane,如果没找到,则从闲置lanes中找到所有挂起的lanes,从中找出优先级最高的lane。 以上都没找到的话,则会返回一个空,跳出更新:

  // wipLanes是正在执行任务的lane,nextLanes是本次需要执行的任务的lane
  // wipLanes !== NoLanes:wipLanes不为空,表示有任务正在执行
  // 如果正在渲染,突然新添加了一个任务,但是这个新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染
  // 如果新任务的优先级比正在执行的任务高,那么则取消当前任务,执行新任务
  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    (wipLanes & suspendedLanes) === NoLanes
  ) {
    const nextLane = getHighestPriorityLane(nextLanes);
    const wipLane = getHighestPriorityLane(wipLanes);
    if (
      nextLane >= wipLane ||
      (nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
    ) {
      return wipLanes;
    }
  }

我们再看ensureRootIsScheduled中是如何处理的:

  const existingCallbackNode = root.callbackNode;
  ...
  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  const existingCallbackPriority = root.callbackPriority;

  // nextLanes获取的是所有任务中优先级最高任务的lane
  // 那么与当前现有的任务的优先级比较,只会有两种结果:
  // 1.与现有的任务优先级一样,那么则会中断当前新任务向下的执行,重用之前现有的任务
  // 2.新任务的优先级大于现有的任务优先级,那么则会取消现有的任务的执行,优先执行优先级高的任务


  // 与现有的任务优先级一样的情况
  if (
    existingCallbackPriority === newCallbackPriority
  ) {
    return;
  }

  // 新任务的优先级大于现有的任务优先级
  // 取消现有的任务的执行
  if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
  }

由于ensureRootIsScheduled是每产生一个更新优先级都是会触发的,当第一次产生后会产生一个对应的任务优先级会交给调度器执行,那么下一次产生相同的更新优先级之后,就会在这里直接返回,而不会产生重复的任务优先级,这个就是react18批量更新的原理,例如:

const App = ()=>{
  const [num , setNum] = useState(0);
  const [count , setCount] = useState(0);
  
  const onClick = ()=>{
    setNum(num + 1)
    setCount(count + 1)
    setNum(n => n + 1)
  }
  
  return (
    <button onClick={onClick}>{num}</button>
  )
}

在这种情况下批量更新就会起作用,产生了3次更新优先级,但是只产生一次任务优先级,前提是他们的优先级相同。 同时如果 existingCallbackNode 和之前的不同而且存在,只能说明这一次产生的优先级更高,因此需要中断之前正在进行或者还未进行的优先级,方法就是cancelCallback(existingCallbackNode),其实就是将Scheduler体系下的堆顶函数置空就行。 通过这样的方式高优先级任务就可以打断低优先级的任务,从而更好的响应用户了。

这里都是onClick事件触发的更新,所以优先级相同都是SyncLane,所以只会产生一个调度任务。

既然只执行一次调度任务,那么要怎么保证3个更新任务都能执行呢?

concurrentQueues会存放所有的更新任务。

function enqueueUpdate(
  fiber: Fiber,
  queue: ConcurrentQueue | null,
  update: ConcurrentUpdate | null,
  lane: Lane,
) {
  concurrentQueues[concurrentQueuesIndex++] = fiber;
  concurrentQueues[concurrentQueuesIndex++] = queue;
  concurrentQueues[concurrentQueuesIndex++] = update;
  concurrentQueues[concurrentQueuesIndex++] = lane;

  concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

  fiber.lanes = mergeLanes(fiber.lanes, lane);
  const alternate = fiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
}

现在获取到了所有任务中优先级最高的lane,和现有任务的优先级existingCallbackPriority,那么nextLanes与当前现有的任务的优先级比较,只会有两种结果:

  1. 与现有的任务优先级一样,那么则会中断当前新任务向下的执行,重用之前现有的任务
  2. 新任务的优先级大于现有的任务优先级,那么则会取消现有的任务的执行,优先执行优先级高的任务,实现高优先级任务插队