从优先级看react-fiber架构

1,748 阅读7分钟

过年玩嗨了,有点飘,状态很差。慢慢的从学习中调整过来。

之前整理了react的fiber架构,由于对架构中的优先级概念理解不深,导致对调度过程的一些细节不明其意。So,在下基于源码和自身对理解整理了下面一篇文章,从优先级的角度来看调度过程。轻点喷。


参考源码基于16.8.6

概要:

  1. 优先级介绍
  2. 事件优先级与事件派发机制
  3. 更新优先级
  4. 任务优先级
  5. 调度优先级
  6. more

一、优先级介绍

不同的事件产生不同的更新(lane),更新被收集成任务,任务会分级调度。

react中有四种优先级:

  1. 事件优先级、
  2. 更新优先级(lane)、
  3. 任务优先级、
  4. 调度优先级(scheulder)

二、事件优先级与事件派发机制

1. 事件优先级

事件优先级分为三类:

  1. 离散事件(discreteEvent),不连续触发的事件,优先级最低为0。比如click,focus,keydown
  2. 用户阻塞事件(userBlockingEvent),连续触发的事件,优先级为1。比如scroll,drag,mouseOut
  3. 连续事件(continousEvent),非触发的连续事件,优先级最高2。比如process、playing、load 参考源码: packages/react-dom/src/event/DOMEventProperties.js
const discreteEventPairsForSimpleEventPlugin = [
  ('cancel': DOMEventName), 'cancel',
  ('click': DOMEventName), 'click',
  ('close': DOMEventName), 'close',
  ('contextmenu': DOMEventName), 'contextMenu',
  ('copy': DOMEventName), 'copy',
  ('cut': DOMEventName), 'cut',
  ('auxclick': DOMEventName), 'auxClick',
  ('dblclick': DOMEventName), 'doubleClick', // Careful!
  ('dragend': DOMEventName), 'dragEnd',
  ('dragstart': DOMEventName), 'dragStart',
  ('drop': DOMEventName), 'drop',
  ('focusin': DOMEventName), 'focus', // Careful!
  ('focusout': DOMEventName), 'blur', // Careful!
  ('input': DOMEventName), 'input',
  ('invalid': DOMEventName), 'invalid',
  ('keydown': DOMEventName), 'keyDown',
  ('keypress': DOMEventName), 'keyPress',
  ('keyup': DOMEventName), 'keyUp',
  ('mousedown': DOMEventName), 'mouseDown',
  ('mouseup': DOMEventName), 'mouseUp',
  ('paste': DOMEventName), 'paste',
  ('pause': DOMEventName), 'pause',
  ('play': DOMEventName), 'play',
  ('pointercancel': DOMEventName), 'pointerCancel',
  ('pointerdown': DOMEventName), 'pointerDown',
  ('pointerup': DOMEventName), 'pointerUp',
  ('ratechange': DOMEventName), 'rateChange',
  ('reset': DOMEventName), 'reset',
  ('seeked': DOMEventName), 'seeked',
  ('submit': DOMEventName), 'submit',
  ('touchcancel': DOMEventName), 'touchCancel',
  ('touchend': DOMEventName), 'touchEnd',
  ('touchstart': DOMEventName), 'touchStart',
  ('volumechange': DOMEventName), 'volumeChange',
];

const otherDiscreteEvents: Array<DOMEventName> = [
  'change',
  'selectionchange',
  'textInput',
  'compositionstart',
  'compositionend',
  'compositionupdate',
];

// prettier-ignore
const userBlockingPairsForSimpleEventPlugin: Array<string | DOMEventName> = [
  ('drag': DOMEventName), 'drag',
  ('dragenter': DOMEventName), 'dragEnter',
  ('dragexit': DOMEventName), 'dragExit',
  ('dragleave': DOMEventName), 'dragLeave',
  ('dragover': DOMEventName), 'dragOver',
  ('mousemove': DOMEventName), 'mouseMove',
  ('mouseout': DOMEventName), 'mouseOut',
  ('mouseover': DOMEventName), 'mouseOver',
  ('pointermove': DOMEventName), 'pointerMove',
  ('pointerout': DOMEventName), 'pointerOut',
  ('pointerover': DOMEventName), 'pointerOver',
  ('scroll': DOMEventName), 'scroll',
  ('toggle': DOMEventName), 'toggle',
  ('touchmove': DOMEventName), 'touchMove',
  ('wheel': DOMEventName), 'wheel',
];
const continuousPairsForSimpleEventPlugin: Array<string | DOMEventName> = [
  ('abort': DOMEventName), 'abort',
  (ANIMATION_END: DOMEventName), 'animationEnd',
  (ANIMATION_ITERATION: DOMEventName), 'animationIteration',
  (ANIMATION_START: DOMEventName), 'animationStart',
  ('canplay': DOMEventName), 'canPlay',
  ('canplaythrough': DOMEventName), 'canPlayThrough',
  ('durationchange': DOMEventName), 'durationChange',
  ('emptied': DOMEventName), 'emptied',
  ('encrypted': DOMEventName), 'encrypted',
  ('ended': DOMEventName), 'ended',
  ('error': DOMEventName), 'error',
  ('gotpointercapture': DOMEventName), 'gotPointerCapture',
  ('load': DOMEventName), 'load',
  ('loadeddata': DOMEventName), 'loadedData',
  ('loadedmetadata': DOMEventName), 'loadedMetadata',
  ('loadstart': DOMEventName), 'loadStart',
  ('lostpointercapture': DOMEventName), 'lostPointerCapture',
  ('playing': DOMEventName), 'playing',
  ('progress': DOMEventName), 'progress',
  ('seeking': DOMEventName), 'seeking',
  ('stalled': DOMEventName), 'stalled',
  ('suspend': DOMEventName), 'suspend',
  ('timeupdate': DOMEventName), 'timeUpdate',
  (TRANSITION_END: DOMEventName), 'transitionEnd',
  ('waiting': DOMEventName), 'waiting',
];

2. 事件派发机制

  1. 事件优先级是创建的时候根据事件名字domEventName确定的
  2. 根据方法getEventPriorityForPluginSystem计算出事件优先级之后,则判断调用不同监听事件
  3. 并使用runWithPriority将调度事件优先级对应的调度优先级写入Scheduler,供计算更新优先级使用
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 根据domEventName计算事件优先级
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  // 根据不同的事件优先级调用不同的监听函数
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

packages/scheduler/Scheduler.js

function unstable_runWithPriority(priorityLevel, eventHandler) {

  var previousPriorityLevel = currentPriorityLevel;
  currentPriorityLevel = priorityLevel;

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}

三、更新优先级

计算更新优先级的大致步骤如下:

  1. 任务优先级的创建取决与当前的调度优先级(即当前的调度情况)。

  2. 首先获取当前Schedule内部的调度优先级Scheduler_getCurrentPriorityLevel,并转换成react内部的调度优先级schedulerPriority,体现在函数getCurrentPriorityLevel中。

  3. 其次根据得到的react内部的调度优先级,转换成车道优先级schedulerLanePriority,体现函数在schedulerPriorityToLanePriority。其中值得注意的是针对离散事件的更新任务处理,其特殊处理为InputDiscreteLanePriority

  4. 最后将车道优先级转换成该更新的车道(即更新优先级),在所有的车道中找到空闲的(~wipLanes)、最高优先级的(lanes & -lanes)车道作为当前更新的优先级。体现函数在findUpdateLane

  5. 言归正传我们着重考察一下lane的计算,体现在requestUpdateLane(fiber)函数中。

export function requestUpdateLane(fiber: Fiber): Lane {
  // Special cases,特殊的情况,
  // 略。。。
  // TODO: Remove this dependency on the Scheduler priority.
  // To do that, we're replacing it with an update lane priority.
  const schedulerPriority = getCurrentPriorityLevel();
  
  // 使用Schedule的优先级会和react内部的优先级耦合,所以用currentUpdateLanePriority来替代。
  // 比如,如果不在Scheduler.runWithPriority,我们获取优先级的时候会拿到正在运行的Scheduler的task的优先级。
  let lane;
  if (
    // 个人理解:离散事件触发的任务特殊处理,返回InputDiscreteLanePriority
    // 用户阻塞和连续事件任务则在下面的schedulerPriorityToLanePriority中处理,返回InputContinuousLanePriority
    (executionContext & DiscreteEventContext) !== NoContext &&
    schedulerPriority === UserBlockingSchedulerPriority
  ) {
    lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
  } else {
    const schedulerLanePriority = schedulerPriorityToLanePriority(
      schedulerPriority,
    );
    // ...省略
    lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  }

  return lane;
}
  1. 我们再看看getCurrentPriorityLevel函数,此函数的作用就是将schedule中优先级转换成react中的优先级
// 源码位置:packages/react-reconciler/src/SchedulerWithReactIntegration.old.js
export function getCurrentPriorityLevel(): ReactPriorityLevel {
  switch (Scheduler_getCurrentPriorityLevel()) {
    case Scheduler_ImmediatePriority:
      return ImmediatePriority;
    case Scheduler_UserBlockingPriority:
      return UserBlockingPriority;
    case Scheduler_NormalPriority:
      return NormalPriority;
    case Scheduler_LowPriority:
      return LowPriority;
    case Scheduler_IdlePriority:
      return IdlePriority;
    default:
      invariant(false, 'Unknown priority level.');
  }
}
// schedule中的优先级
export const unstable_ImmediatePriority = 1;
export const unstable_UserBlockingPriority = 2;
export const unstable_NormalPriority = 3;
export const unstable_IdlePriority = 5;
export const unstable_LowPriority = 4;

// react中的优先级
export const ImmediatePriority: ReactPriorityLevel = 99;
export const UserBlockingPriority: ReactPriorityLevel = 98;
export const NormalPriority: ReactPriorityLevel = 97;
export const LowPriority: ReactPriorityLevel = 96;
export const IdlePriority: ReactPriorityLevel = 95;
  1. 我们再来看schedulerPriorityToLanePriority,此函数的作用就是将schedulePriority转换成scheduleLanePriority
export function schedulerPriorityToLanePriority(
  schedulerPriorityLevel: ReactPriorityLevel,
): LanePriority {
  switch (schedulerPriorityLevel) {
    case ImmediateSchedulerPriority:
      return SyncLanePriority;
    case UserBlockingSchedulerPriority:
      return InputContinuousLanePriority;
    case NormalSchedulerPriority:
    case LowSchedulerPriority:
      // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
      return DefaultLanePriority;
    case IdleSchedulerPriority:
      return IdleLanePriority;
    default:
      return NoLanePriority;
  }
}
  1. 最后我们看看最重要的findUpdateLane函数,找出空闲车道(~wipLanes)中最高优先级的车道(lanes & -lanes
export function findUpdateLane(
  lanePriority: LanePriority,
  wipLanes: Lanes,
): Lane {
  switch (lanePriority) {
    case NoLanePriority:
      break;
    case SyncLanePriority:
      return SyncLane;
    case SyncBatchedLanePriority:
      return SyncBatchedLane;
    case InputDiscreteLanePriority: {
      const lane = pickArbitraryLane(InputDiscreteLanes & ~wipLanes);
      if (lane === NoLane) {
        return findUpdateLane(InputContinuousLanePriority, wipLanes);
      }
      return lane;
    }
    case InputContinuousLanePriority: {
      const lane = pickArbitraryLane(InputContinuousLanes & ~wipLanes);
      if (lane === NoLane) {
        return findUpdateLane(DefaultLanePriority, wipLanes);
      }
      return lane;
    }
    case DefaultLanePriority: {
      let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);
      if (lane === NoLane) {
        lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
        if (lane === NoLane) {         
          lane = pickArbitraryLane(DefaultLanes);
        }
      }
      return lane;
    }
    case TransitionPriority: // Should be handled by findTransitionLane instead
    case RetryLanePriority: // Should be handled by findRetryLane instead
      break;
    case IdleLanePriority:
      let lane = pickArbitraryLane(IdleLanes & ~wipLanes);
      if (lane === NoLane) {
        lane = pickArbitraryLane(IdleLanes);
      }
      return lane;
    default:
      break;
  }
}

更新的本质就是创建一个update对象,并为之计算出一个lane(优先级),并将此update对象以单项链表的结构存入当前fiber对象的sharedQueue属性中。

四、任务优先级

  1. 首先进入scheduleUpdateOnFiber,此函数是所有调度任务的入口。
  2. 拿到上一步获取的lane,将lane合并至root.pendingLanes,合并标记整颗fiber树,并设置各个车道的过期时间
  3. 之后开始计算任务优先级,首先会去处理过期的车道的任务,防止低优先级的任务饿死。
  4. 计算逻辑主要是从pendingLanes中的车道类型分情况处理(空闲任务、非空闲任务)
  5. 值得注意的是,当正在渲染时,仅有当前是高优先级的车道任务才能抢占低优先级车道的任务

1. 所有调度的入口函数,所有调度的开始

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  checkForNestedUpdates();
  // 将跟新优先级向上合并,直至root
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  // 将更新优先级合并
  markRootUpdated(root, lane, eventTime);
  
  // 只考察优先级的代码,所以省略...
}

2. 标记整颗fiber树节点的lane

export function markRootUpdated(
  root: FiberRoot,
  updateLane: Lane,
  eventTime: number,
) {
    // 将更新优先级收集
    root.pendingLanes |= updateLane;
    // 不暂停相同或更低优先级的更新
    const higherPriorityLanes = updateLane - 1; 
    root.suspendedLanes &= higherPriorityLanes;
    root.pingedLanes &= higherPriorityLanes;
    // 将任务更新开始时间收集
    const eventTimes = root.eventTimes;
    const index = laneToIndex(updateLane);
    eventTimes[index] = eventTime;
}

3. 计算任务优先级

export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
  const pendingLanes = root.pendingLanes;  
  let nextLanes = NoLanes;
  let nextLanePriority = NoLanePriority;

  const expiredLanes = root.expiredLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;

  // 处理低优先级的任务,防止饿死
  if (expiredLanes !== NoLanes) {
    nextLanes = expiredLanes;
    nextLanePriority = return_highestLanePriority = SyncLanePriority;
  } else {
   // 计算出下一个任务的车道lane,和车道优先级lanePriority
    const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
    if (nonIdlePendingLanes !== NoLanes) {      
      const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
      if (nonIdleUnblockedLanes !== NoLanes) {
      	// 非空闲、非阻塞??
        nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
        nextLanePriority = return_highestLanePriority;
      } else {
      	// 非空闲、暂停的
        const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
        if (nonIdlePingedLanes !== NoLanes) {
          nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
          nextLanePriority = return_highestLanePriority;
        }
      }
    } else {
      // 对空闲任务的处理
      const unblockedLanes = pendingLanes & ~suspendedLanes;
      if (unblockedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(unblockedLanes);
        nextLanePriority = return_highestLanePriority;
      } else {
        if (pingedLanes !== NoLanes) {
          nextLanes = getHighestPriorityLanes(pingedLanes);
          nextLanePriority = return_highestLanePriority;
        }
      }
    }
  }
  nextLanes = pendingLanes & getEqualOrHigherPriorityLanes(nextLanes);

  // 如果任务正在渲染,高优先级任务将抢占低优先级任务
  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    (wipLanes & suspendedLanes) === NoLanes
  ) {
    getHighestPriorityLanes(wipLanes);
    const wipLanePriority = return_highestLanePriority;
    if (nextLanePriority <= wipLanePriority) {
      return wipLanes;
    } else {
      return_highestLanePriority = nextLanePriority;
    }
  }  
  return nextLanes;
}

5. 任务调度的核心方法

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;
  
  // 收集到期的车道root.expiredLanes |= lane;
  markStarvedLanesAsExpired(root, currentTime);

  // 计算任务优先级,下一个可以的任务lanes
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  // 计算新的任务的优先级
  const newCallbackPriority = returnNextLanesPriority();

  // 复用、取消、新建任务的代码,这里不做展示,只考察上面计算优先级逻辑的代码
  // 省略...
  
  
  // 生成出新任务之后,将任务优先级赋值
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

任务优先级的意义在于比较并处理新、老优先级的更新逻辑。高优先级的新任务抢占低优先级的老任务,相同优先级的新、任务收敛,低优先级的新任务等待高优先级任务。

五、调度优先级

  1. 调度优先级是Scheduler内部的优先级 pachages/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;

  1. 任务的调度根据优先级来分别创建不同延时的任务
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;
  // 创建newtask
  return newTask;
}