React调度原理

前言

本文是React源码学习系列第二篇,该系列整体都基于React18.0.0版本源码。旨在学习过程中记录一些个人的理解。该篇介绍React Scheduler模块的实现原理。

React调度分为同步和异步,同步的会根据宿主是否支持微任务做不同处理,同步的会使用微任务进行调度,不支持微任务和异步都会使用Scheduler模块进行调度

调度策略

同步

支持微任务

  1. 向syncQueue队列里面插入回调函数。
  2. 微任务queueMicrotask -> Promise -> setTimeout 根据宿主环境支持情况降级处理
  3. 注册一个微任务,在回调函数里面执行syncQueue里面的回调函数。
js 复制代码
let syncQueue = null;
let callback = performSyncWorkOnRoot.bind(null, root);
if (syncQueue === null) {
    syncQueue = [callback];
} else {
    syncQueue.push(callback);
}
scheduleMicrotask(() => {
    for (let i = 0; i < queue.length; i++) {
        let callback = queue[i];
        do {
            callback = callback(isSync);
        } while (callback !== null);
    }
});

不支持微任务

不支持微任务的,以最高优先级通过Scheduler调度,本质是添加一个宏任务。

js 复制代码
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);

异步

根据lanes算出优先级,然后已该优先级调度,计算方式参考上文-优先级转化。

js 复制代码
scheduleCallback(schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root));

Scheduler

Scheduler主要做的事情就是各种优先级的任务的调度及中断、恢复。内部包含两个循环,一个是队列循环,还有一个是单个任务调度循环,在调度执行时间较长的任务时,会去判断执行时间是否超过了单帧的时间(5ms),如果超过了会让出主线程,等待下一帧的空闲时间继续调度。

要完成以上功能涉及到几个问题。

  1. 任务队列如何保存。
  2. 任务的执行时机。
  3. 低优先级任务的饥饿问题。

优先级队列

Scheduler采用小顶堆保存任务队列。

  1. 是一棵完全二叉树(除最后一层外,其他层的节点个数都是满的,且最后一层靠左排列)
  2. 堆中每一个节点的值都小于等于其子树的每一个值。
  3. 适合用数组保存,用数组下标代替指向左右节点的指针,数组下标i的左右子节点分别为2i+1、2i+2。
  4. 包含push、pop、peek三个方法。
  5. push 向堆中推入数据。
  6. pop从 堆顶取出数据。
  7. peek 获取"排序依据最小的值"的节点。
  8. push、pop操作的时候都会对堆进行重新排序。
  9. peek方法获取堆中最小节点时间复杂度为O(1)。

堆化操作

push

js 复制代码
export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node); // 加入最后
  siftUp(heap, node, index); // 开始排序
}
function siftUp(heap, node, i) {
  let index = i;
  while (index > 0) {
    // 2i + 1 2i + 2 是子节点
    // 反操作就是父节点
    // let num = (index -1)/ 2; Math.floor(num);
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    // 当前节点比父节点小
    if (compare(parent, node) > 0) {
      // 交换父子节点位置
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // 退出
      return;
    }
  }
}

pop

js 复制代码
export function pop(heap: Heap): Node | null {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop(); // 取出最后一个
  if (last !== first) {
    heap[0] = last; // 放到第一个 开始排序
    siftDown(heap, last, 0);
  }
  return first;
}
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  // 二叉树高度最高只有数组长度的一半,只要对每一层进行对比,不需要遍历length次,优化
  const halfLength = length >>> 1; 
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1; // 左边子节点索引
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1; // 右边边子节点索引
    const right = heap[rightIndex];
    // 左边子节点比当前节点小
    if (compare(left, node) < 0) {
      // 右边子节点比左边子节点小 说明右边子节点更小
      if (rightIndex < length && compare(right, left) < 0) {
        // 跟右边子节点交换位置
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        // 说明左边子节点更小
        // 跟左边子节点交换位置
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && compare(right, node) < 0) { // node比左边子节点大,比右边子节点小
      // 跟右边子节点交换位置
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // node小于左右子节点
      return;
    }
  }
}

任务的执行时机

理想的方法是在每一帧的空闲时间可以执行多次,并且尽早的执行调度多种优先级的任务。

pass方案

  1. requestIdeCallback设计初衷是"能够在事件循环中执行低优先级的工作,减少对动画、用户输入等高优先级事件的影响"。与Scheduler的多种优先级的调度策略不符。
  2. requestAnimationFrame定义回调函数会在浏览器下次Paint之前执行。相当于每一帧只能执行一次。不满足一帧时间内可以执行多次。

采用方案

  1. setImmediate
  2. MessageChannel
  3. setTimeout

根据宿主环境的支持情况依次降级。

js 复制代码
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    setImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  schedulePerformWorkUntilDeadline = () => {
    setTimeout(performWorkUntilDeadline, 0);
  };
}

过期时间

不同优先级的调度任务会加上对应的时间,算出任务的过期时间。 ImmediateSchedulerPriority优先级调度的task的过期时间是task开始时间-1,所以立马会过期,后面render阶段不可打断。

js 复制代码
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250; 
var NORMAL_PRIORITY_TIMEOUT = 5000; 
var LOW_PRIORITY_TIMEOUT = 10000; 
// 0b111111111111111111111111111111 
var IDLE_PRIORITY_TIMEOUT = 1073741823;

解决饥饿问题

并发模式下,如果一直有高优先级的task加入,那么低优先级的task就会一直得不到调度或者执行过程中被打断。

  1. 异步调度的task如果过期了,不可打断。 shouldTimeSlice由下面三个条件决定:
  2. 不包含阻塞的lane,没有用户输入事件。
  3. 不包含过期的lane,不包含root.expiredLanes中的lane。
  4. 调度的任务未过期

源码梳理

scheduleCallback方法暴露给React进行任务调度。接受三个参数

  1. priorityLevel 调度优先级
  2. callback 调度的执行函数
  3. options 配置项
js 复制代码
// 保留关键代码
function 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;
  // 创建一个task
  var newTask = {
    id: taskIdCounter++,
    callback, // callback = performConcurrentWorkOnRoot
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  // 延时任务
  if (startTime > currentTime) {
    newTask.sortIndex = startTime;
    // 推入timerQueue队列
    push(timerQueue, newTask);
    // taskQueue队列为空并且timerQueue只有刚创建的task
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 是否有setTimeout方法发起的延迟调度。
      if (isHostTimeoutScheduled) {
        // 取消
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // requestHostTimeout就是一个setTimeout,startTime - currentTime后执行 handleTimeout
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 非延时任务 推入taskQueue
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    // 是否在调度任务
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // 发起一个调度
      requestHostCallback(flushWork);
    }
  }
  return newTask;
}

根据传入的优先级、回调函数及延迟时间创建一个任务,根据当前时间和优先级计算任务过期时间,维护到taskQueue或timerQueue队列中。队列以过期时间排序,过期时间最小的排在最前面。 requestHostCallback方法

js 复制代码
function requestHostCallback(callback) {
  // 把任务赋值给全局变量
  scheduledHostCallback = callback;
  //判断是否有任务在调度,初始为false
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

schedulePerformWorkUntilDeadline就是上文"任务的执行时机"选出来的三个方案。根据宿主支持情况调用performWorkUntilDeadline方法。 performWorkUntilDeadline方法。

js 复制代码
 // 调度时候执行的函数
const performWorkUntilDeadline = () => {
  // scheduledHostCallback为flushWork
  if (scheduledHostCallback !== null) { 
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      // scheduledHostCallback为我们requestHostCallback传入的函数 flushwork,实则执行 workLoop
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      // 表示是否还有任务需要执行,taskqueue不为空
      if (hasMoreWork) {
        // 被中断了重新发起调度,对应第二个小循环
        schedulePerformWorkUntilDeadline();
      } else {
        // 重置状态
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
};

其实就是调用了flushwork方法,我们再来看看flushwork方法。

js 复制代码
function flushWork(hasTimeRemaining, initialTime) {
  isHostCallbackScheduled = false;
  // 有延时任务调度,取消
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

最终调用的是workLoop方法。

js 复制代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // 检查是否有过期任务需要从timeQueue添加到taskQueue中的
  advanceTimers(currentTime);
  // 取出taskQueue中第一个任务。
  currentTask = peek(taskQueue);
  // 第一个大循环
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
      // 执行中会根据当前任务执行的时间是否超过一帧渲染的时间和用户是否与界面有交互来判断是否应该中断当前任务
    ) {
      // 任务没有过期,该帧的空闲时间时间已经没有了,跳出循环
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; // 任务是否过期
      //执行 performConcurrentWorkOnRoot
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      // 同一个任务因为时间切片到期,恢复callback,下次调度继续执行
      if (typeof continuationCallback === 'function') { 
        currentTask.callback = continuationCallback;
      } else {
        // 执行完了,把该任务推出队列
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    // 执行完了,把该任务推出队列
    currentTask = peek(taskQueue);
  }
  // 表示中断了,taskqueue没执行完。
  if (currentTask !== null) {
    return true 表示还有工作,在performWorkUntilDeadline会继续发起调度 hasMoreWork
    return true;
  } else {
    // taskqueue执行完了,有timerTask,则挂一个延时任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

requestHostTimeout方法就是挂一个延时任务,即setTimeout方法。

js 复制代码
setTimeout(handleTimeout, firstTimer.startTime - currentTime);

到这里Scheduler模块的主要功能就讲完了,跟React交互的部分会专门写一篇文章。主要涉及到优先级的选择。

React和Scheduler结合

优先级

为了保持两个包的独立性,他们没有共用一套优先级,而是分别有直接的优先级设置。

Scheduler共分为5种优先级。

js 复制代码
const ImmediatePriority = 1;  // 同步优先级
const UserBlockingPriority = 2; // 会阻塞用户输入的优先级
const NormalPriority = 3;  // 默认优先级
const LowPriority = 4; // 不重要的优先级
const IdlePriority = 5; // 空闲优先级

React共4种优先级。

  1. DiscreteEventPriority 离散型事件优先级(click、 focus、blur)等
  2. ContinuousEventPriority 连续事件优先级(drag、mousemove、scroll、wheel)等
  3. DefaultEventPriority 默认优先级
  4. IdleEventPriority 空闲情况优先级
js 复制代码
const DiscreteEventPriority = SyncLane;
const ContinuousEventPriority = InputContinuousLane; 
const DefaultEventPriority = DefaultLane; 
const IdleEventPriority = IdleLane;

Lane模型

采用32位二进制,除去最高位符号位,可以表示31种优先级。这种模式可以很方便的表示一个优先级或者一批优先级。

  1. 选出一个优先级:每次会选出优先级最高的update,并以这个优先级作为调度的优先级。
  2. 表示"批"的概念:基于选出的某个优先级,计算出属于同一批的所有优先级。比如纠缠、或者有依赖的

示例

js 复制代码
const [num, setNum] = useState(0);
const [isPending, startTransition] = useTransition();
startTransition(() => {
   setNum((num) => num + 1);
})
setNum((num) => num + 100); 

第二个update的优先级高于第一个update,但是它依赖第一个,所以会在同一批调度执行。

优先级转化

从React优先级到Scheduler需要经过两次转化。

1.把lanes转为React优先级

js 复制代码
function lanesToEventPriority(lanes: Lanes): EventPriority {
  // 取最右边为1的位,即最高的优先级
  const lane = getHighestPriorityLane(lanes); 
  // lane比DiscreteEventPriority优先级高则为DiscreteEventPriority
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}

2.React优先级转化为Scheduler优先级

js 复制代码
let schedulerPriorityLevel;
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中可以触发更新的方法。

  1. root.render() --- HostRootFiber
  2. this.setState() --- ClassComponent
  3. this.forceUpdate() --- ClassComponen
  4. useState dispatcher --- FunctionComponen
  5. useReducer dispatcher --- FunctionComponen 本文以root.render()举例说明React与Scheduler是如何协同调度更新任务的。

root.render()

js 复制代码
// 保留关键代码
ReactDOMRoot.prototype.render = function(
  children: ReactNodeList,
): void {
  const root = this._internalRoot; // FiberRootNode
  updateContainer(children, root, null, null);
};

调度前准备阶段

该阶段主要是根据以下数据创建一个Update,并把Update挂到Fiber的UpdateQueue中。

  1. eventTime:更新发生事件
  2. lane:根据优先级生成一个lane
js 复制代码
// 保留关键代码
function updateContainer(
  element: ReactNodeList, // 虚拟dom babel通过jsx方法生成
  container: OpaqueRoot, // fiberRootNode
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  const current = container.current; // hostRootFiber
  const eventTime = requestEventTime(); // 记录更新发生时间
  const lane = requestUpdateLane(current); // 生成一个更新lane
  const update = createUpdate(eventTime, lane); // 创建一个update,update为更新最小单元
  update.payload = {element};
  callback = callback === undefined ? null : callback; // 是否有回调函数,render的第二个参数,ClassComponent的this.setState方法的第二个参数
  if (callback !== null) {
    update.callback = callback;
  }
  enqueueUpdate(current, update, lane); // 加入更新队列
  const root = scheduleUpdateOnFiber(current, lane, eventTime); // 从HostRootFiber节点开始调度更新
  return lane;
}

createUpdate方法

js 复制代码
// 保留关键代码
function createUpdate(eventTime: number, lane: Lane): Update<*> {
  const update: Update<*> = {
    eventTime,
    lane,

    tag: UpdateState, // 产生更新的类型
    payload: null,
    callback: null,

    next: null, // 链表结构,指向下一个update
  };
  return update;
}

enqueueUpdate方法

js 复制代码
// 保留关键代码
function enqueueUpdate<State>(
  fiber: Fiber,
  update: Update<State>,
  lane: Lane,
) {
  const updateQueue = fiber.updateQueue;
  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
  const pending = sharedQueue.pending;
  if (pending === null) {
     update.next = update;
  } else {
     update.next = pending.next;
     pending.next = update;
  }
  sharedQueue.pending = update; // pending指向最后一个update, pending.next指向第一个update
}

requestUpdateLane方法,先查看是否有手动设置的优先级,有的话返回,没有则根据触发更新的事件,返回一个对应的优先级。

js 复制代码
// 保留关键代码
function requestUpdateLane(fiber: Fiber): Lane {
  // .....省略渲染阶段产生的更新、过度更新
  // 手动设置的优先级
  const updateLane: Lane = getCurrentUpdatePriority();
  if (updateLane !== NoLane) {
    return updateLane;
  }
  // 根据事件类型获取优先级
  const eventLane: Lane = getCurrentEventPriority();
  return eventLane;
}

render更新并非事件触发,所有返回默认优先级DefaultEventPriority

js 复制代码
// 保留关键代码
function getCurrentEventPriority(): * {
  const currentEvent = window.event;
  if (currentEvent === undefined) {
    return DefaultEventPriority; // 0b0000000000000000000000000010000
  }
  // 根据触发的事件类型,分配不同的优先级
  return getEventPriority(currentEvent.type);
}

冒泡阶段

该阶段从发生更新的Fiber开始,一路向上直到根Fiber节点HostRootFiber,沿途更新父Fiber节点lanes及childLanes。并返回HostRootFiber。

js 复制代码
// 保留关键代码
function scheduleUpdateOnFiber(
  fiber: Fiber, // 发起调度的fiber
  lane: Lane,
  eventTime: number,
): FiberRoot | null {
  // 从当前发起更新Fiber到HostRootFiber的一个向上找寻过程,
  // 1.主要用于更新路径上Fiber的lanes及childLanes
  // 2.返回FiberRootNode 如果没有找到FiberRootNode 返回null
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }
  // 1.root.pendingLanes 加上本次更新的lane 
  // 2.root.eventTimes 加上eventTime
  markRootUpdated(root, lane, eventTime);
  ensureRootIsScheduled(root, eventTime);
  return root;
}

markUpdateLaneFromFiberToRoot方法

js 复制代码
// 保留关键代码
function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  lane: Lane,
): FiberRoot | null {
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); // 合并lane
  let alternate = sourceFiber.alternate;
  if (alternate !== null) { // current Fiber树存在的话,合并lane
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
  let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) { // 向上找到HostRootFiber
    parent.childLanes = mergeLanes(parent.childLanes, lane); // 把子Fiber的lane合并到父Fiber的childLanes上
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    }
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;
    return root; // 返回FiberRootNode
  } else {
    return null;
  }
}

mergeLanes方法

js 复制代码
// 合并lanes
function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}

markRootUpdated方法

js 复制代码
// 保留关键代码
function markRootUpdated(
  root: FiberRoot,
  updateLane: Lane,
  eventTime: number,
) {
  root.pendingLanes |= updateLane;
  const eventTimes = root.eventTimes;
  const index = laneToIndex(updateLane); // 找到当前车道的前置0的个数 即找到该lane的索引
  eventTimes[index] = eventTime;
}

开始调度

ensureRootIsScheduled方法,首先检查FiberRootNode.pendingLanes中的lanes是否过期,过期的会加到首先检查FiberRootNode.expiredLanes属性上,然后从FiberRootNode.pendingLanes中选出优先级最高的一个优先级(nextLanes)作为本次更新的调度优先级,并根据选出的lane再选出一批lanes在本次一起参与调度。跟当前正在进行的优先级(wipLanes)做对比,有以下几种情况。

  1. nextLanes为空。直接return 取消现有调度任务,并结束本次调度。
  2. nextLanes优先级高于wipLanes。中断现有调度任务,重新发起一个调度。
  3. nextLanes优先级等于wipLanes。结束本次调度,继续执行现有任务。
  4. nextLanes优先级低于wipLanes。结束本次调度,继续执行现有任务。

调度方式参考上文"调度策略"。

js 复制代码
// 保留关键代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode; // 当前正在更新的任务
  // 检查所有的非suspended的lane设置过期时间。
  // 如果lane已经过期,加入expiredLanes。
  markStarvedLanesAsExpired(root, currentTime);
  // 选出本次更新的一批lanes
  // nextLanes有三种可能
  // 1.NoLanes
  // 2.workInProgressRootRenderLanes 当前正在更新的lanes
  // 3.选出来的更高优先级的lanes
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  // 如果是空 直接return
  if (nextLanes === NoLanes) {
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode); // 取消先有的更新任务
    }
    // 重置
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }
  // 使用nextLanes中最高优先级的lane来表示本次调度的优先级。lane转换为优先级
  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  const existingCallbackPriority = root.callbackPriority; // 当前的更新优先级
  if (existingCallbackPriority === newCallbackPriority) {
    // 优先级没有改变。我们可以重用现有的任务。退出。
    return;
  }
  // 走到这里 说明信更新优先级比现在执行的高
  // 取消现有调度任务
  if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
  }
  // 重新发起一个调度
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) { // 同步优先级
    // 向syncQueue队列里面插入回调函数
    scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    // 根据是否支持微任务区别处理同步优先级更新
    // 支持微任务
    if (supportsMicrotasks) {
      // queueMicrotask -> Promise -> setTimeout 根据浏览器支持情况降级处理
      scheduleMicrotask(() => {
         if (executionContext === NoContext) {
           flushSyncCallbacks(); // 执行syncQueue里面的回调函数
         }
      });
    } else {
      // 不支持微任务的,以最高优先级通过Scheduler调度,本质是添加一个宏任务
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    // 同步更新不能打断,所以执行完重置它
    newCallbackNode = null;
  } else { // 非同步优先级
    let schedulerPriorityLevel;
    // 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;
    }
    // 根据优先级调度 此处为Scheduler包的内容
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  // 更新FiberRootNode的属性
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

getNextLanes方法,选出本次更新的一批lanes

js 复制代码
// 保留关键代码
function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
  // 如果没有未完成的工作,直接返回 NoLanes。
  const pendingLanes = root.pendingLanes;
  if (pendingLanes === NoLanes) {
    return NoLanes;
  }

  let nextLanes = NoLanes;
  const suspendedLanes = root.suspendedLanes; // 挂起的lanes
  const pingedLanes = root.pingedLanes;
  // 从所有待更新的lanes中取出非空闲lanes
  const nonIdlePendingLanes = pendingLanes & NonIdleLanes; 
  if (nonIdlePendingLanes !== NoLanes) {
    // 去掉suspended的lanes
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes; 
    if (nonIdleUnblockedLanes !== NoLanes) {
      // 从lanes中取最高优先级lane
      nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes); 
    } else {
      // 从suspended恢复执行的lanes
      const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes; 
      if (nonIdlePingedLanes !== NoLanes) {
        // 从lanes中取最高优先级lane
        nextLanes = getHighestPriorityLanes(nonIdlePingedLanes); 
      }
    }
  } else { // 剩下的都是空闲车道
    // 去掉suspended的lanes
    const unblockedLanes = pendingLanes & ~suspendedLanes;  
    if (unblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLanes(unblockedLanes);
    } else {
      if (pingedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLanes(pingedLanes);
      }
    }
  }
  if (nextLanes === NoLanes) {
    return NoLanes;
  }
  // wipLanes不等于NoLanes,并且不等于nextLanes,即当前有一批lanes在更新
  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    (wipLanes & suspendedLanes) === NoLanes // wipLanes都是suspended的Lanes
  ) {
    // 各自取出最高的优先级的lane
    const nextLane = getHighestPriorityLane(nextLanes);
    const wipLane = getHighestPriorityLane(wipLanes);
    // wipLane优先级高于nextLane
    if (nextLane >= wipLane) {
      return wipLanes; // 返回当前更新的lanes
    }
  }
  // 此处省略lanes纠缠相关代码
  return nextLanes;
}

执行调度任务

有同步和并发两种模式执行更新任务,performSyncWorkOnRoot,performConcurrentWorkOnRoot。本篇以并发模式举例。 performConcurrentWorkOnRoot方法。根据以下条件判断是否开启并发模式。

  1. 不包含阻塞lane
  2. lanes没有过期lane
  3. 该任务没有过期

renderRootConcurrent方法,跟renderRootSync方法的区别就是更新任务是否可以被中断。

scss 复制代码
// 保留关键代码
function performConcurrentWorkOnRoot(root, didTimeout) {
  // 缓存当前的更新任务
  const originalCallbackNode = root.callbackNode;
  // 执行useEffect,确保上一次调度的所有副作用都执行完毕
  const didFlushPassiveEffects = flushPassiveEffects();
  if (didFlushPassiveEffects) {
    if (root.callbackNode !== originalCallbackNode) {
      // 当前任务被取消。退出。
      return null;
    } else {
      // 当前任务未被取消。继续下去。
    }
  }
  // 获取下一次要更新的lanes
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  if (lanes === NoLanes) {
    return null;
  }
  // 是否开启时间切片 满足以下三个条件开启
  // 1.不包含阻塞lane
  // 2.lanes没有过期lane
  // 3.该任务没有过期
  // disableSchedulerTimeoutInWorkLoop默认false
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) && 
    !includesExpiredLane(root, lanes) && 
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);
  // 根据是否开启时间切片 调用同步或者并发render
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
 
  if (exitStatus !== RootInProgress) {
    //忽略错误处理代码
    const finishedWork: Fiber = (root.current.alternate:any);
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    // 提交阶段
    finishConcurrentRender(root, exitStatus, lanes);
  }
  // 工作未完成,退出之前,确保为下一个pending发起调度。
  ensureRootIsScheduled(root, now());
  if (root.callbackNode === originalCallbackNode) {
    // 说明是同一个任务因为时间切片耗尽而暂停。
    // 返回一个函数,Scheduler里面有判断,如果返回函数会继续调度
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

到这里一个完整的调度过程就讲完了,接下去就是React的Render流程了。会专门开一篇文章。

参考

React设计原理 - 卡颂

图解:最小堆构建、存储、插入、删除过程

相关推荐
等下吃什么?4 小时前
NEXT.js 创建postgres数据库-关联github项目-连接数据库-在项目初始化数据库的数据
react.js
小白小白从不日白7 小时前
react 高阶组件
前端·javascript·react.js
奶糖 肥晨10 小时前
react是什么?
前端·react.js·前端框架
B.-1 天前
Remix 学习 - @remix-run/react 中主要的 hooks
前端·javascript·学习·react.js·web
盼盼盼2 天前
如何避免在使用 Context API 时出现状态管理的常见问题?
前端·javascript·react.js
小白小白从不日白2 天前
react 动画_样式处理
前端·react.js
等下吃什么?2 天前
NEXT.js 中间件 NextResponse.redirect 无效
react.js
小白小白从不日白2 天前
react hooks--useState
前端·javascript·react.js
盼盼盼2 天前
React 项目中,如何实现有效的内存管理和防止内存泄漏?
前端·react.js·前端框架
坚持学习前端日记2 天前
react使用技巧
前端·react.js·前端框架