重学React18(二):scheduleUpdateOnFiber任务调度

1,019 阅读7分钟

image.png 这几天看到Scheduler最近新增了Task的类型,也就是要添加到队列的任务;其中的callback属性,是实际要执行的更新等操作;这里提个小问题,callback既然是要执行的任务回调,什么时候会是null呢?

重学React18(一):render函数中写到了scheduleUpdateOnFiber,这一篇就来看下react的任务调度,会找到答案~

scheduleUpdateOnFiber

scheduleUpdateOnFiber(packages\react-reconciler\src\ReactFiberWorkLoop.old.js)是react18每一次更新都会调用的方法,为方便理解,对源码做一些简化,先看下主要过程:

export function (
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // * 省略掉一些逻辑判断,初次渲染不会走到,更新时才会走到
  // ...scheduleUpdateOnFiber
  // Mark that the root has a pending update.
  markRootUpdated(root, lane, eventTime);  // * 将lane合并到root.pendingLanes上。root.pendingLanes |= lane; suspendedLanes, pingedLanes设置为NoLanes,后面getNextLanes会用到
  // ...
  ensureRootIsScheduled(root, eventTime); 
  // ...
}

其中ensureRootIsScheduled是更新的必经之路,负责不同优先级任务的调度,并产生调度优先级

// Use this function to schedule a task for a root. There's only one task per
// root; if a task was already scheduled, we'll check to make sure the priority
// of the existing task is the same as the priority of the next level that the
// root has work on. This function is called on every update, and right before
// exiting a task.
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;

  // Determine the next lanes to work on, and their priority.
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
  );
  // ...
  // * 取出接下来要处理的最紧急任务lanes中的最高优先级
  // We use the highest priority lane to represent the priority of the callback.
  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  
  // * 跟当前任务优先级比较, 优先级相同则复用,直接return
  // Check if there's an existing task. We may be able to reuse it.
  const existingCallbackPriority = root.callbackPriority;
  if (existingCallbackPriority === newCallbackPriority) {
    // The priority hasn't changed. We can reuse the existing task. Exit.
    return;
  }

  // * 优先级不同,则取消当前任务,`scheduleCallback`生成一个新的调度,并更新`root`
  // * 初次渲染,root.callbackPriority为NoLane,即0,所以会直接调用scheduleCallback
  if (existingCallbackNode != null) {
    // Cancel the existing callback. We'll schedule a new one below.
    cancelCallback(existingCallbackNode);
  }

  // Schedule a new callback.
  let newCallbackNode;
  
  // ...
  // 如果本地调度优先级最高的lane是SyncLane,则进入【同步调度】
  // scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); 将performSyncWorkOnRoot添加到syncQueue队列
  // 然后利用scheduleMicrotask在微任务中遍历syncQueue并同步执行所有performSyncWorkOnRoot回调
  // 下面是非同步过程
  let schedulerPriorityLevel = // ...;  // * 通过条件判断,将lanes转化成相应的调度优先级:ImmediateSchedulerPriority | UserBlockingSchedulerPriority | NormalSchedulerPriority | IdleSchedulerPriority;
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

通过函数命名和注释,可以知道ensureRootIsScheduled的过程:

  • 取出下一个最高优先级的任务
  • 跟当前任务优先级比较
  • 优先级相同则复用,直接return
  • 优先级不同,则取消当前任务,scheduleCallback生成一个新的任务,并更新root

scheduleCallback

接下来,就要看下scheduleCallbackperformConcurrentWorkOnRoot了,为避免篇幅过长,本文先看一下前者,梳理任务调度流程,后者放在下一篇。

scheduleCallback对应的是Scheduler.js中的unstable_scheduleCallback, 看这个方法之前,我们先看下Scheduler.js中定义的一些全局变量, 其中最主要的就是这两个队列

// Tasks are stored on a min heap
var taskQueue: Array<Task> = [];   // 即将进行调度的任务的集合(不需要延时和延时时间到了的任务)
var timerQueue: Array<Task> = [];  // 需要延时执行的任务的集合

React需要先处理优先级高的任务,所以taskQueuetimerQueue都采用了小顶堆的排序方式。

因为task中的排序字段sortIndex取的是expirationTimestartTime, 值越小,优先级越高。小顶堆每次取出堆顶元素,就是优先级最高的任务啦☺

关于堆的pushpop等操作都在packages\scheduler\src\SchedulerMinHeap.js文件中,可以去看一下这些方法的实现,加深对堆操作的理解。

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: {delay: number},
): Task {
  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 = // ...; // * 根据调度优先级priorityLevel设置相应的超时时间:NORMAL_PRIORITY_TIMEOUT | IMMEDIATE_PRIORITY_TIMEOUT | USER_BLOCKING_PRIORITY_TIMEOUT | LOW_PRIORITY_TIMEOUT | IDLE_PRIORITY_TIMEOUT

  var expirationTime = startTime + timeout;

  // * 这里可以看到Scheduler中的任务的结构了,其中callback属性,也就是被调度后执行的方法,是传入的performConcurrentWorkOnRoot或者performSyncWorkOnRoot
  var newTask: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  if (startTime > currentTime) {  // * 有delay的情况
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask); // * 将当前任务添加到timerQueue,并按照sortIndex(startTime)重新排列小顶堆
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // * taskQueue为空,并且当前任务是可以最先执行的延时任务
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.  // * 取消定时器,定时器是由requestHostTimeout创建的
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime); // * 重新创建一个定时器
    }
  } else { // * 没有delay的情况
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

立即执行任务

先看下立即执行的任务的这个条件分支

  • 任务的sortIndex设置为expirationTime
  • 将任务添加到taskQueue,并且按照sortIndex,重新调整成小顶堆
  • requestHostCallback(flushWork),即flushWork赋值给全局变量scheduledHostCallback,然后触发performWorkUntilDeadline
// * 根据环境,选择使用setImmediate、MessageChannel、setTimeout中的一个方法
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};


function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;
    const hasTimeRemaining = true;

    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `scheduledHostCallback` errors, then
    // `hasMoreWork` will remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        port.postMessage(null);
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};

可以看到上面的performWorkUntilDeadline调用flushWork, 还没看到flushWork的实现,不过我们可以猜测flushWork应该是不断执行taskQueue中的任务,直到没有TimeRemaining了,返回taskQueue中是否还有待执行的任务,等待下一次执行,是不是有requestIdleCallback内味儿啦~

延时任务

  • 任务的sortIndex设置为startTime
  • 将任务添加到timerQueue,并且按照sortIndex,重新调整成小顶堆
  • 判断下taskQueue是否为空,当前任务是不是timerQueen中需要最先调度的任务
  • 上面的条件成立的话,requestHostTimeout(handleTimeout, startTime - currentTime),即重新创建一个定时器,延时时间是delay,回调方法是handleTimeout

那么,主要逻辑就是handleTimeout了

function handleTimeout(currentTime: number) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime); // * 取出timerQueue中所有startTime<=currentTime的任务,放入taskQueue,准备接受调度

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) { // * taskQueue不为空,执行其中任务的callback
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else { // * taskQueue为空,检查下timeQueue,重复上面放入taskQueue的逻辑
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

以上,就是react18任务调度的整体流程,再来总结一下

  1. render阶段,创建了一个update对象,并挂载到fiber的UpdateQueue上,然后调用scheduleUpdateOnFiber
  2. 调用markRootUpdated,将update对象的lane合并到root.pendingLanes,表示有更新
  3. 调用ensureRootIsScheduled进行调度
  4. 从root.pendingLanes中取出接下来要处理的任务,和它们中的最高优先级
  5. 最高优先级,放在root.callbackPriority上,以便下次调度的时候做比较,看是否需要取消已有的任务,
  6. 将事件优先级转化成schedulerPriorityLevel
  7. 调用scheduleCallback,通过schedulerPriorityLevel和回调,创建一个task,并挂载到root.callbackNode, 表示当前已经有任务被调度了
  8. 通过taskQueue和timerQueue,实现立即执行任务和延时任务的执行

问题

  1. callback是每个任务被调度的时候,执行的方法,就是函数组件的更新等,那既然被调度了,什么情况会是null呢(答案我们在上面的调度过程已经梳理过了,具体实现就在cancelCallback方法中哦~)
  2. 浏览器环境下,requestIdleCallback改成MessageChannel的好处是什么
  3. 新旧任务的优先级不同,会取消旧的任务,新任务的优先级一定比旧任务的优先级高吗