react任务调度(下篇)

135 阅读4分钟

一、任务调度的入口

无论是首次渲染,还是后续更新操作,都会进入到react-reconciler对外暴露的updateContainer函数,然后在该函数中调用scheduleUpdateOnFiber,因此scheduleUpdateOnFiber函数是任务调度的入口。

函数的调用顺序:

updateContainer -> scheduleUpdateOnFiber(任务输入入口)-> ensureRootIsScheduled(安排调度任务)-> unstable_scheduleCallback(创建调度任务)-> 调用requestHostCallback

二、ensureRootIsScheduled(安排调度任务)

如果任务类型是并发任务,则需要经过调度,会通过scheduleCallback回调函数注册调度任务。

// 使用这个函数为 root 安排任务,每个 root 只有一个任务
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // Schedule a new callback.
  // 重新安排一个新的渲染任务
  let newCallbackNode;
  // 如果新渲染任务的优先级是同步优先级 if 逻辑处理的是同步任务,同步任务不需经过 Scheduler
  if (newCallbackPriority === SyncLane) {
    // 同步任务不经过 Scheduler,任务执行入口是 performSyncWorkOnRoot 函数
    if (root.tag === LegacyRoot) {
      // LegacyRoot 启动模式 把这个渲染任务加入 syncQueue 队列中
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      // 启动模式为非 legacy模式  把这个渲染任务加入 syncQueue 队列中
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
  } else {
    // else 逻辑处理的是并发任务,并发任务需要经过 Scheduler 根据调度优先级,调度并发任务
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
}

三、unstable_scheduleCallback(创建调度任务)

scheduleCallback 函数是引用了 packages/scheduler/src/Scheduler.js 路径下的 unstable_scheduleCallback 函数,通过unstable_scheduleCallback函数创建调度任务(newTask),newTask的callback实际上就是performConcurrentWorkOnRoot。然后根据任务是否超时,分别将任务插入到超时队列timerQueue 和调度任务队列taskQueue中。将任务插入调度任务队列taskQueue之后,会通过requestHostCallback函数去调度任务。

// 省略部分无关代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 1. 获取当前时间
  var currentTime = getCurrentTime();
  var startTime;
  if (typeof options === 'object' && options !== null) {
    // 从函数调用关系来看, 在v17.0.2中,所有调用 unstable_scheduleCallback 都未传入options
    // 所以省略延时任务相关的代码
  } else {
    startTime = currentTime;
  }
  // 2. 根据传入的优先级, 设置任务的过期时间 expirationTime
  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;
  // 3. 创建新任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (startTime > currentTime) {
    // 省略无关代码 v17.0.2中不会使用
  } else {
    newTask.sortIndex = expirationTime;
    // 4. 加入任务队列
    push(taskQueue, newTask);
    // 5. 请求调度
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }
  return newTask;
}

四、调用requestHostCallback进行调度

4.1 requestHostCallback的调用

创建任务之后, 最后请求调度requestHostCallback(flushWork), flushWork函数作为参数被传入调度中心内核等待回调.。requestHostCallback函数在上文调度内核中已经介绍过了, 在调度中心中, 只需下一个事件循环就会执行回调, 最终执行flushWork。

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

// 请求回调
requestHostCallback = function(callback) {
  // 1. 保存callback
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    // 2. 通过 MessageChannel 发送消息
    port.postMessage(null);
  }
};

4.2 flushWork的调用

flushWork中调用了workLoop. 队列消费的主要逻辑是在workLoop函数中, 这就是React 工作循环一文中提到的任务调度循环.

// 省略无关代码
function flushWork(hasTimeRemaining, initialTime) {
  // 1. 做好全局标记, 表示现在已经进入调度阶段
  isHostCallbackScheduled = false;
  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    // 2. 循环消费队列
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    // 3. 还原全局标记
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

4.3 workloop的调用

workloop函数实际上是一个大循环,会不断的去队列中获取到任务。任务执行之后会有回调,回调的结果是函数的话就会继续执行,如果不是函数的话就会将当前的任务移除队列。然后currentTask重新赋值,新的值是从队列取出的新的任务,知道 currentTask 的值为null才打断循环。最后会return true或者false来通知任务是否执行结束。

// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
  currentTask = peek(taskQueue); // 获取队列中的第一个任务
  while (currentTask !== null) {
    if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {
      // 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 此时,执行 callback,即 performConcurrentWorkOnRoot 方法
      // 在执行 performConcurrentWorkOnRoot 方法的过程中,如果 reconciler 中的 workLoop 中断了
      // 会返回 performConcurrentWorkOnRoot 自身方法,也就是 continuationCallback 会被放到当前 task 的 callback
      // 此时 workLoop 的 while 循环中断,但是由于当前 task 并没有从队列中出来,
      // 所以下一次执行 workLoop 时,仍然会执行本次存储的 continuationCallback
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      // 回调完成, 判断是否还有连续(派生)回调
      if (typeof continuationCallback === 'function') {
        // 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
        currentTask.callback = continuationCallback;
      } else {
        // 把currentTask移出队列
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
    } else {
      // 如果任务被取消(这时currentTask.callback = null), 将其移出队列
      pop(taskQueue);
    }
    // 更新currentTask
    currentTask = peek(taskQueue);
  }
  if (currentTask !== null) {
    return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
  } else {
    return false; // task队列已经清空, 返回false.
  }
}

shouldYieldToHost 函数详解:是否让出主线程,注意shouldYieldToHost的判定条件:

  • currentTime >= deadline: 只有时间超过deadline之后才会让出主线程(其中deadline = currentTime + yieldInterval).

  • yieldInterval默认是5ms, 只能通过forceFrameRate函数来修改(事实上在 v17.0.2 源码中, 并没有使用到该函数).

  • 如果一个task运行时间超过5ms, 下一个task执行之前, 会把控制权归还浏览器.

  • navigator.scheduling.isInputPending(): 用于判断是否有输入事件(包括: input 框输入事件, 点击事件等)

    // 是否让出主线程 shouldYieldToHost = function() { const currentTime = getCurrentTime(); if (currentTime >= deadline) { if (needsPaint || scheduling.isInputPending()) { return true; } return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms, 这个判断只在初始化过程中才有可能返回false } else {. return false; }