React源码解析之优先级Lane模型上

·  阅读 2773

概述

Lane是React中用于表示任务的优先级。优先级分为高优先级与低优先级,当用户操作界面时,为了避免页面卡顿,需要让出线程的执行权,先执行用户触发的事件,这个我们称之为高优先级任务,其它不那么重要的事件我们称之为低优先级任务。

不同优先级的任务间,会存在一种现象:当执行低优先级任务时,突然插入一个高优先级任务,那么会中断低优先级的任务,先执行高优先级的任务,我们可以将这种现象称为任务插队。当高优先级任务执行完,准备执行低优先级任务时,又插入一个高优先级任务,那么又会执行高优先级任务,如果不断有高优先级任务插队执行,那么低优先级任务便一直得不到执行,我们称这种现象为任务饥饿问题

不同的优先级机制

React中有三套优先级机制:

  1. React事件优先级
  2. Lane优先级
  3. Scheduler优先级

React事件优先级

// 离散事件优先级,例如:点击事件,input输入等触发的更新任务,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout触发的更新任务
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane;
复制代码

可以看到React的事件优先级的值还是使用的Lane的值,那为什么不直接使用Lane呢?我觉得可能是为了不与Lane机制耦合,后面事件优先级有什么变动的话,可以直接修改而不会影响到Lane。

Lane优先级转换为React事件优先级:

export function lanesToEventPriority(lanes: Lanes): EventPriority {
  // 找到优先级最高的lane
  const lane = getHighestPriorityLane(lanes);
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}
复制代码

Scheduler优先级

export const NoPriority = 0; //没有优先级
export const ImmediatePriority = 1; // 立即执行任务的优先级,级别最高
export const UserBlockingPriority = 2; // 用户阻塞的优先级
export const NormalPriority = 3; // 正常优先级
export const LowPriority = 4; // 较低的优先级
export const IdlePriority = 5; // 优先级最低,闲表示任务可以闲置
复制代码

React事件优先级转换为Scheduler优先级

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
    ...
    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;
    }
}
复制代码

lanesToEventPriority函数就是上面Lane优先级转换为React事件优先级的函数,先将lane的优先级转换为React事件的优先级,然后再根据React事件的优先级转换为Scheduler的优先级。

Lane优先级

// lane使用31位二进制来表示优先级车道共31条, 位数越小(1的位置越靠右)表示优先级越高
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: Lanes = /*            */ 0b0000000000000000000000000000100;

// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /*                    */ 0b0000000000000000000000000010000;

// 过度优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /*                       */ 0b0000000001000000000000000000000;

const RetryLanes: Lanes = /*                            */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /*                             */ 0b0000100000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;

export const SelectiveHydrationLane: Lane = /*          */ 0b0001000000000000000000000000000;
复制代码

可以看到lane使用31位二进制来表示优先级车道,共31条, 位数越小(1的位置越靠右)表示优先级越高。

上面简单的介绍了一下React中的优先级机制,下面我们正式开启源码解析。

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

当我们触发一个React事件产生一个任务时,它会自带一个优先级,那么这个优先级是怎么赋予这个事件的?

当我们项目第一次渲染时:

const root = document.getElementById('root');
ReactDOM.createRoot(root).render(<div>Hello World</div>);
复制代码

调用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产生一个更新任务的时候:

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

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

enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst); //获取当前组件对应的fiber节点
    const eventTime = requestEventTime(); // 获取当前事件触发的时间
    const lane = requestUpdateLane(fiber); // 获取到当前事件对应的Lane优先级

    // 创建更新对象,将需要更新的内容挂载到payload上
    const update = createUpdate(eventTime, lane); 
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      update.callback = callback;
    }

    // 将更新对象添加进更新队列中
    enqueueUpdate(fiber, update, lane);
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    ...
}
复制代码

获取事件的优先级

首先获取到当前需要更新的组件的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添加到更新对象上:

const update = createUpdate(eventTime, lane); 

export function createUpdate(eventTime: number, lane: Lane): Update<*> {
  const update: Update<*> = {
    eventTime, //更新事件触发的时间
    lane, // 事件更新的优先级

    tag: UpdateState, // 类型:更新,替换,强制更新等
    payload: null, // 需要更新的内容
    callback: null, // 更新回调 setState的第二个参数

    next: null, // 下一个更新对象
  };
  return update;
}
复制代码

接着将需要更新的内容挂载到payload上,将更新回调函数挂载到更新对象的callback属性上:

update.payload = payload;
if (callback !== undefined && callback !== null) {
  update.callback = callback;
}
复制代码

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

 enqueueUpdate(fiber, update, lane);
复制代码

更新队列是一个循环链表结构。

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

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
): FiberRoot | null {
  // 检查是否做了无限循环更新,比如:在render函数中调用了setState,如果是则会报错提示
  checkForNestedUpdates();
  ...

  // 收集需要更新的子节点的lane,存放在父fiber上的childLanes上
  // 更新当前fiber节点的lannes,表示当前节点需要更新
  // 从当前需要更新的fiber节点向上遍历,遍历到根节点(root fiber)并更新每个fiber节点上的childLanes属性
  // childLanes有值表示当前节点下有子节点需要更新
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }

  // 将当前需要更新的lane添加到fiber root的pendingLanes属性上,表示有新的更新任务需要被执行
  // 通过计算出当前lane的位置,并添加事件触发时间到eventTimes中
  markRootUpdated(root, lane, eventTime);
  ...

  ensureRootIsScheduled(root, eventTime);
  ...
  return root;
}
复制代码

我们看到函数内部调用了markUpdateLaneFromFiberToRoot,这个函数主要的作用是更新当前fiber节点的lannes,表示当前节点需要更新,然后收集需要更新的子节点的lane,存放在父fiber上的childLanes属性上。在后面做更新时,会根据fiber节点上lannes*判断当前fiber节点是否需要更新,根据childLanes判断当前fiber的子节点是否需要更新。我们来看一下markUpdateLaneFromFiberToRoot内部是如何实现的:

function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  lane: Lane,
): FiberRoot | null {
  // 更新当前节点的lanes,表示当前节点需要更新
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  let alternate = sourceFiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
 ...
  // 从当前需要更新的fiber节点向上遍历直到根fiber节点(root fiber),更新每个fiber节点的childLanes
  // 在之后会通过childLanes来判断当前fiber节点下是否有子节点需要更新
  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 {
      if (__DEV__) {
        if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
          warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
        }
      }
    }
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;
    return root;
  } else {
    return null;
  }
}
复制代码

首先将新任务的lane当前fiber节点上的lanes属性,表示当前fiber需要更新,如果fiber节点对应的alternate不为空的话,表示是在更新,并且会同步更新alternate上的lanes。

// 更新当前节点的lanes,表示当前节点需要更新
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
}
复制代码

接下来则是从当前更新的节点向上遍历至fiber根节点(root fiber),更新每个fiber节点上的childLanes属性,表示当前fiber下的子节点需要更新:

  // 从当前需要更新的fiber节点向上遍历直到根fiber节点(root fiber),更新每个fiber节点的childLanes
  // 在之后会通过childLanes来判断当前fiber节点下是否有子节点需要更新
  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);
    }
    node = parent;
    parent = parent.return;
  }
复制代码

sourceFiber.return的意思是获取当前fiber节点的父级,后面也会出一章关于fiber的详解,这边就简单的提一下。

遍历更新完成后,则会返回fiber root节点。

markUpdateLaneFromFiberToRoot执行完毕,紧接着调用了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;
}
复制代码

eventTimes是31位长度的Array,对应Lane使用31位的二进制。

假设updateLane为:0b000100 那么它在eventTimes中则是这种形式的:

[-1, -1, 44573.3452, -1, -1...]
复制代码

markRootUpdated调用完成后,紧接着调用了ensureRootIsScheduled函数,准备开始任务的调度。

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

任务饥饿问题

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

关于任务饥饿问题的处理,主要逻辑在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) {
    // 获取当前lanes中最左边1的位置
    // 例如:
    // lanes = 28 = 0b0000000000000000000000000011100
    // 以32位正常看的话,最左边的1应该是在5的位置上
    // 但是lanes设置了总长度为31,所以我们可以也减1,看作在4的位置上
    // 如果这样不好理解的话,可以看pickArbitraryLaneIndex中的源码:
    // 31 - clz32(lanes), clz32是Math中的一个API,获取的是最左边1前面的所有0的个数
    const index = pickArbitraryLaneIndex(lanes);

    // 上面获取到最左边1的位置后,还需要获取到这个位置上的值
    // index = 4
    // 16 = 10000 = 1 << 4
    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, 每次循环删除一个,直到lanes等于0
    // 例如:
    // lane = 16 =  10000
    // ~lane =      01111
    // lanes = 28 = 11100
    // lanes = 12 = 01100 = lanes & ~lane
    lanes &= ~lane;
  }
}
复制代码

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

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

lanes = 28 = 0b0000000000000000000000000011100
复制代码

然后使用了:

27 = Math.clz32(lanes);
复制代码

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

const index = 31 - 27; // 4
复制代码

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

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

 expirationTimes[index] = computeExpirationTime(lane, currentTime);
复制代码

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

if (expirationTime <= currentTime) {
  root.expiredLanes |= lane;
}
复制代码

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

 lanes &= ~lane;
复制代码

任务插队

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;
}
复制代码

处理完饥饿问题,接下来调用了getNextLanes获取到所有任务中优先级最高的任务的lane,我们来看下其中的源码:

export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
  const pendingLanes = root.pendingLanes;

  //没有剩余任务的时候,跳出更新
  if (pendingLanes === NoLanes) {
    return NoLanes;
  }
  ...
}
复制代码

首先判断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。

以上都没找到的话,则会返回一个空,跳出更新:

//从pendingLanes中找不到有任务,则返回一个空
if (nextLanes === NoLanes) {
    return NoLanes;
}
复制代码

如果当前又任务正在执行,那么wipLanes就不等于空,需要将新的任务的优先级与正在执行的任务的优先级进行比较。如果新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染,反之,新任务的优先级比正在执行的任务高,那么则取消当前任务,先执行新任务:

  // 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);
  }
复制代码

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

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

任务调度开始

接下来则是开始创建任务,执行任务调度:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  ...

  // 开始调度任务
  // 判断新任务的优先级是否是同步优先级
  // 是则使用同步渲染模式,否则使用并发渲染模式(时间分片)
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    ...
    newCallbackNode = null;
  } else {
    let schedulerPriorityLevel;
    // lanesToEventPriority函数将lane的优先级转换为React事件的优先级,然后再根据React事件的优先级转换为Scheduler的优先级
    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;
    }
    //将react与scheduler连接,将react产生的事件作为任务使用scheduler调度
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}
复制代码

判断新任务的优先级是否是同步优先级,是则使用同步渲染模式,否则使用并发渲染模式使用scheduler调度任务, 在使用并发模式时,会将lane的优先级转换为React事件的优先级,然后再根据React事件的优先级转换为Scheduler的优先级,Scheduler会根据它自己的优先级给任务做时间分片。

由于篇幅太长,我准备把接下来更新部分lanes的使用写到React源码解析之优先级Lane模型下中,笔芯...

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改