React源码解析之Scheduler

解析源码前,我们明确几个问题:

  1. Scheduler是什么,作用是什么?
  2. Scheduler的出现是为了解决什么问题?

Scheduler是一个任务调度器,它会根据任务的优先级对任务进行调用执行。
在有多个任务的情况下,它会先执行优先级高的任务。如果一个任务执行的时间过长,Scheduler会中断当前任务,让出线程的执行权,避免造成用户操作时界面的卡顿。在下一次恢复未完成的任务的执行。

Scheduler是一个独立的包,不仅仅在React中可以使用。

基本概念

以此我们引出Scheduler的两个核心点:任务队列管理任务的中断与恢复

接下来我们就带着两个问题去解析源码:

  1. 任务队列是如何管理的?
  2. 任务是怎么执行的,执行时是怎么被中断的,然后又是怎么恢复执行的?

详细流程

Scheduler (1).png

Reac通过下面的代码让Fiber树的构建进入调度流程:

   function ensureRootIsScheduled(root: FiberRoot, currentTime: number){
       ...
       let schedulerPriorityLevel;
       // 通过lanesToEventPriority函数将lane优先级转化为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),
        );
   }
复制代码

为什么这里需要做一次优先级的转换呢?因为React和Scheduler都是相对独立的,它们自己内部都有自己的一套优先级机制,所以当React产生的事件需要被Scheduler调度时,需要将React的事件优先级转换为Scheduler的调度优先级。

调度入口-scheduleCallback

接下来我们点进去查看scheduleCallback内部代码:

function scheduleCallback(priorityLevel, callback) {
  ...
  return Scheduler_scheduleCallback(priorityLevel, callback);
}
复制代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
 ......
}
复制代码

这个方法就是react与Scheduler连接的函数。

下面我们详细分析一下Sheduler的基本配置:

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; // 优先级最低,表示任务可以闲置(在没有任务执行的时候,才会执行闲置的任务)
复制代码

Scheduler中的任务管理队列

Scheduler中有两个任务队列:timerQueue 和 taskQueue。
timerQueue 和 taskQueue都是最小堆的数据结构。

  1. timerQueue:所有没有过期的任务会放在这个队列中。
  2. taskQueue:所有过期的任务会放在该队列中,并且按过期时间排序,过期时间越小则排在越前面,并且越先执行。

当Scheduler开始地调度任务执行时,首先会从taskQueue过期任务队列中获取任务执行,一个任务执行完成则会从taskQueue中弹出,当taskQueue中所有的任务都执行完成了,那么则会去timerQueue中检查是否有过期的任务,有的话则会拿出放到taskQueue中去执行。

下面来看一下具体的源码:

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; //任务过期时间

  //以react产生的事件来创建一个新的任务
  var newTask = {
    id: taskIdCounter++,
    callback, // callback = performConcurrentWorkOnRoot
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  //scheduler有两个任务队列:timerQueue 和 taskQueue
  //timerQueue中存放延时任务,也就是未过期的任务
  //taskQueue中存放的是过期任务,也就是需要立即执行的任务
  //timerQueue 和 taskQueue是一个最小堆的数据结构

  //对任务开始时间与当前时间进行比较
  //任务开始时间大于当前时间,表示当前任务是一个延时任务
  if (startTime > currentTime) {
    // This is a delayed task.
    //将开始时间作为排序id,越小排在越靠前
    newTask.sortIndex = startTime;
    //将新建的任务添加进延时任务队列中
    push(timerQueue, newTask);

    //当过期任务队列中执行完所有的任务,
    //则需要不断遍历延时队列中的任务,一旦有任务过期则需要立即添加到过期任务队列中进行执行
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      //当前是否有requestHostTimeout(就是一个setTimeout)正在执行,有的话则停止,避免多个requestHostTimeout一起运行,造成资源的不必要浪费
      //重新调用requestHostTimeout检查延时队列中是否有过期任务
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      // 创建一个timeout作为调度者
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    //将过期时间作为排序id,越小排在越靠前
    newTask.sortIndex = expirationTime;
    //将新建的任务添加进过期任务队列中
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    //判断是都已有Scheduled正在调度任务
    //没有的话则创建一个调度者开始调度任务,有的话则直接使用上一个调度者调度任务
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}
复制代码

scheduleCallback中主要是创建一个新的任务,并且根据任务的开始时间来判断任务是否过期,针对未过期的任务则会添加到timerQueue中,使用startTimer做为排序的依据。如果taskQueue中任务全部执行完成,则会调用requestHostTimeout,实际上这个函数是创建了一个setTimeout,把第一个任务的超时时间作为setTimeout的时间间隔调用handleTimeout。
那么handleTimeout中又做了哪些事情,我们来看下源码:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;

  //检查延时任务队列中是否有已过期的任务
  //有的话则将过期任务拿出添加到过期任务队列中进行执行
  advanceTimers(currentTime);

  //isHostCallbackScheduled判断是否已经发起过调度
  //如果当前没有正在执行的调度,则会创建一个调度去执行任务
  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}
复制代码

handleTimeout中主要是检查timerQueue中是否有已过期的任务,有的话则会将已过期的任务添加到taskQueue中去执行。这项工作主要是advanceTimers这个函数去来实现的:

function advanceTimers(currentTime) {
  //检查延时任务队列中是否有已过期的任务
  //有的话则将过期任务拿出添加到过期任务队列中进行执行
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}
复制代码

针对过期的任务,则会将过期时间作为排序依据,然后调用requestHostCallback函数创建调度者开始调度流程。

if (!isHostCallbackScheduled && !isPerformingWork) {
  isHostCallbackScheduled = true;
  requestHostCallback(flushWork);
}
复制代码

创建调度者-requestHostCallback

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}
复制代码

这里我们先记住callback是调用requestHostCallback传入的flushWork函数,会在后面调用。
schedulePerformWorkUntilDeadline则是创建调度者真正的函数,我们来看下它的实现:

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  // 使用setImmediate的主要原因是因为在服务端渲染,MessageChannel会阻止nodejs的进程退出
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // 使用MessageChannel的原因是因为
  // setTimeout如果嵌套的层级超过了 5 层,并且 timeout 小于 4ms,则设置 timeout 为 4ms。
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  //在以上方案都不能实现的时候,则降级使用setTimeout来实现创建调度者
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}
复制代码

关于setImmediateMessageChannel这里就不做详细介绍,后面想单独写一期关于事件循环的文章时再回过头来看,自己对于这两个API应该也会新的理解。

schedulePerformWorkUntilDeadline函数主要是创建一个调度者,并调用performWorkUntilDeadline函数发起任务的调度。
performWorkUntilDeadline函数中则会调用任务的执行函数开始执行任务,那么接下来我们则会重点讲解一下任务的执行,中断和恢复

任务执行-performWorkUntilDeadline

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    
    try {
      //调用scheduledHostCallback函数,开始任务的执行 scheduledHostCallback = flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {

      //hasMoreWork是workLoop函数最后返回的值
      //表示是否还有任务需要执行
      //如果为true表示有任务在执行中被中断,需要重新执行,那么则需要重新发起一个调度
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        //hasMoreWork为false表示taskQueue中的任务都执行完成了
        //需要将调度者释放,为下一次调度做准备
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};
复制代码

performWorkUntilDeadline内部调用的scheduledHostCallback函数,是在调用requestHostCallback时赋值为了flushWork函数。

接下来我们看一下flushWork函数中干了什么:

function flushWork(hasTimeRemaining, initialTime) {
 ...
 return workLoop(hasTimeRemaining, initialTime);
 ...
}
复制代码

其函数内部最终调用了workLoop函数,然后将wookLoop的返回值返回了出去,也就是performWorkUntilDeadline中的hasMoreWork的值。从这里可以看出真正执行任务的地方就在wookLoop函数中。

任务的中断和恢复

关于任务的执行,有两个重要的特点:中断和恢复。记得之前我们介绍Scheduler的作用时,它主要的功能是对任务的执行时间进行检查,任务执行时间过长则会中断任务,之后再会对未执行完成的任务恢复执行。那么它是怎么做的呢,接下来我们来看一下wookloop的结构:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  //检查是否有过期任务需要添加到taskQueue中执行的
  advanceTimers(currentTime);

  //取出第一个任务(添加任务时以过期时间作为排序依据,过期时间越小排在越前面,表示执行的优先级越高)
  currentTask = peek(taskQueue);

  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    //当前任务过期时间是否大于当前时间,大于则表示没有过期则不需要立即执行
    //hasTimeRemaining: 表示是否还有剩余时间,剩余时间不足则需要中断当前任务,让其他任务先执行
    //shouldYieldToHost: 是否应该中断当前任务
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }

    //callback是performConcurrentWorkOnRoot函数
    //判断callback是否不为空,为空则会将当前任务从任务队列中删除,所以scheduler想要删除任务会将任务的callback设置为空
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      //回到函数为null,则表示任务执行完成,会从任务队列中删除
      currentTask.callback = null;
      //获取任务的优先级
      currentPriorityLevel = currentTask.priorityLevel;
      //判断当前任务是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      //获取执行任务完成后的结果
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        //任务执行完成后的结果返回的是一个函数表示当前任务没有完成
        //则将这个函数作为当前任务新的回调函数,在下一次While循环时调用
        //concurrent模式下,callback是performConcurrentWorkOnRoot函数,其内部originalCallbackNode为当前正在执行的任务
        //会与root.callbackNode上挂载的任务比较,如果不相同则表示任务执行完毕,如果相同,则表示任务没有执行完成,
        //返回自身,作为当前任务新的回调函数,接下来则会让出执行权给优先级更高的任务先执行
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      //检查延时队列中是否有过期的任务
      advanceTimers(currentTime);
    } else {
      //删除当前任务
      pop(taskQueue);
    }
    //从taskQueue中继续获取任务,如果上一次任务没有完成,那么不会从taskQueue中删除,获取的还是上一次任务
    //接下来会继续执行它
    currentTask = peek(taskQueue);
  }

  //当前任务被中断,currentTask则不会为null,则会返回true,
  //scheduler会继续发起调度,执行任务
  if (currentTask !== null) {
    return true;
  } else {
    //currentTask为null,则表示taskQueue中的任务都执行完成了
    //则判断timerQueue中是否有任务,有任务的话会去检查是否有过期任务
    //有的话则添加到taskQueue中,并重新发起调度执行任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}
复制代码

wookloop函数中主要是循环taskQueue执行任务
首先取出第一个任务赋值给currentTask变量,然后开始进入循环。

我们看到进入循环的第一段代码:

if (
  currentTask.expirationTime > currentTime &&
  (!hasTimeRemaining || shouldYieldToHost())
) {
  break;
}
复制代码

这段代码是中止当前任务的关键。

中止判断条件:

  1. currentTask.expirationTime > currentTim:首先会判断当前任务的过期时间是否大于当前时间,大于则说明当前任务还没有过期不用现在执行,先将执行权让给已过期的任务。
  2. !hasTimeRemaining:表示是否还有剩余时间,剩余时间不足则需要中断当前任务,让其他任务先执行,hasTimeRemaining一直为true,我们可以暂时忽略这个条件。
  3. shouldYieldToHost函数:
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false;
  }
 
  if (enableIsInputPending) {
    if (needsPaint) {
      return true;
    }
    if (timeElapsed < continuousInputInterval) {
      if (isInputPending !== null) {
        return isInputPending();
      }
    } else if (timeElapsed < maxInterval) {
      if (isInputPending !== null) {
        return isInputPending(continuousOptions);
      }
    } else {
      return true;
    }
  }
  return true;
}
复制代码

首先检查当前任务的使用时间是否小于帧间隔时间,小于则返回false表示无需中断,

startTime是在调用performWorkUntilDeadline时赋的值,也就是任务开始调度的时候的开始时间:

const performWorkUntilDeadline = () => {
  ...
    startTime = currentTime;
  ...
};
复制代码

如果大于表示当前任务的执行时间超过了一帧渲染的时间5ms,会让用户操作造成卡顿,则返回true表示需要中断。

关于isInputPending它的作用是检测用户的输入事件,例如:鼠标点击,键盘输入等,如果有用户输入测返回true,没有则返回false。

任务执行

接下里则是执行任务:

//callback是performConcurrentWorkOnRoot函数
//判断callback是否不为空,为空则会将当前任务从任务队列中删除,所以scheduler想要删除任务会将任务的callback设置为空
const callback = currentTask.callback;

if (typeof callback === 'function') {
  //回到函数为null,则表示任务执行完成,会从任务队列中删除
  currentTask.callback = null;
  //获取任务的优先级
  currentPriorityLevel = currentTask.priorityLevel;
  //判断当前任务是否过期
  const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
  if (enableProfiling) {
    markTaskRun(currentTask, currentTime);
  }
  //获取执行任务完成后的结果
  const continuationCallback = callback(didUserCallbackTimeout);
  currentTime = getCurrentTime();
  if (typeof continuationCallback === 'function') {
    //任务执行完成后的结果返回的是一个函数表示当前任务没有完成
    //则将这个函数作为当前任务新的回调函数,在下一次While循环时调用
    //concurrent模式下,callback是performConcurrentWorkOnRoot函数,其内部originalCallbackNode为当前正在执行的任务
    //会与root.callbackNode上挂载的任务比较,如果不相同则表示任务执行完毕,如果相同,则表示任务没有执行完成,
    //返回自身,作为当前任务新的回调函数,接下来则会让出执行权给优先级更高的任务先执行
    currentTask.callback = continuationCallback;
  } else {
    if (currentTask === peek(taskQueue)) {
      pop(taskQueue);
    }
  }
  //检查延时队列中是否有过期的任务
  advanceTimers(currentTime);
} else {
  //删除当前任务
  pop(taskQueue);
}
复制代码

首先从currentTask当前任务中获取任务执行函数callback

callback实际就是调用scheduleCallback时传入的performConcurrentWorkOnRoot函数:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  ...
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  ...
}
复制代码

然后判断了callback的类型是否为function,如果不是则会从taskQueue中删除该任务,如果是则会执行该回调函数,然后回调函数返回执行结果continuationCallbackcontinuationCallback可以看作为当前任务的执行状态,当continuationCallback值为null时则表示当前任务执行完成,如果为function则表示当前任务未执行完成,在执行过程中被打断,需要先要让出执行权给优先级更高的任务先执行。

执行状态

那么执行执行状态如果判断的呢?

首先我们先来看回顾一下调用scheduleCallback的函数ensureRootIsScheduled中的源码:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  ...
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root),
  );

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

可以看到,调用scheduleCallback函数后会将任务对象newTask返回回来,并挂载到root的callbackNode属性上

接下来我们进入callback函数中,也就是performConcurrentWorkOnRoot中看一下具体是如果判断执行状态的:

function performConcurrentWorkOnRoot(root, didTimeout) {
  const originalCallbackNode = root.callbackNode;
  
  ...
  
  let exitStatus =
    shouldTimeSlice(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout)
      ? renderRootConcurrent(root, lanes)
      : renderRootSync(root, lanes);
  
  if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}
复制代码

从源码中我们看到,首先将root.callbackNode赋值给了originalCallbackNode变量,中间会调用renderRootConcurrent或者renderRootSync方法,那么我们可以判定到root.callbackNode肯定会在这两个方法中被消耗掉。

然后可以看到root.callbackNodecommit阶段被置为了null

function commitRootImpl(root, renderPriorityLevel) {
  ...
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  ...
}
复制代码

react整个构建流程大致可以分为两个阶段:

  1. render阶段,在这个阶段任务是可以被中断的
  2. commit阶段,这个阶段任务是无法被中断的

我们回过头再来看一下performConcurrentWorkOnRoot函数中的代码:

function performConcurrentWorkOnRoot(root, didTimeout) {
  ...
  
  if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}
复制代码

如果当任务执行到commit阶段,那么任务肯定已经完成了,root.callbackNode会被置为null,那么if判断肯定是不想等的,所以会返回null,那么workloop中的continuationCallback的值也会null,表示任务已执行完成。

那么任务在执行过程中被中断了呢?

我们来看一下并发渲染函数renderRootConcurrent

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  ...
  
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  
  ...

}
复制代码

其内部调用了workLoopConcurrent函数:

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
复制代码

可以看到这个函数在循环创建workInProgress树,并且调用了shouldYield函数,之前我们有解析过shouldYield函数中主要是检查当前任务的执行时间是否大于一帧所渲染的时间,并且会使用isInputPendingAPI来判断用户是否对页面有交互,如果满足其中一个条件则会中断当前任务。中断任务则不会继续向下执行,也就不会执行到commit阶段root.callbackNode会也不会被置为null

那么root.callbackNode 则会等于 originalCallbackNode,那么就会进入if判断返回performConcurrentWorkOnRoot函数。

那么我们回过头再看一下workLoop中的代码:

//获取执行任务完成后的结果
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
    //任务执行完成后的结果返回的是一个函数表示当前任务没有完成
    //则将这个函数作为当前任务新的回调函数,在下一次While循环时调用
    //concurrent模式下,callback是performConcurrentWorkOnRoot函数,其内部originalCallbackNode为当前正在执行的任务
    //会与root.callbackNode上挂载的任务比较,如果不相同则表示任务执行完毕,如果相同,则表示任务没有执行完成,
    //返回自身,作为当前任务新的回调函数,接下来则会让出执行权给优先级更高的任务先执行
    currentTask.callback = continuationCallback;
} 

//检查延时队列中是否有过期的任务
advanceTimers(currentTime);

//从taskQueue中继续获取任务,如果上一次任务没有完成,那么不会从taskQueue中删除,获取的还是上一次任务
//接下来会继续执行它
currentTask = peek(taskQueue);
复制代码

当前任务未完成时,是不会从taskQueue中删除的,而是会将返回的函数continuationCallback重新赋值给当前任务的callback属性,然后会检查在执行过程中是否有过期的任务需要执行,有的话则会添加到taskQueue中。

如果当前任务是由于执行时间过长导致中断的话,peek(taskQueue)取出的还是上一个未完成执行的任务,会继续执行。

如果是由于高优先级的任务导致的中断,peek(taskQueue)取出的则是优先级最高的任务来执行。

当currentTask为null或者是被判断任务的条件所中断:

while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    ...
}
复制代码

那么就走到下面:

if (currentTask !== null) {
    return true;
  } else {
    //currentTask为null,则表示taskQueue中的任务都执行完成了
    //则判断timerQueue中是否有任务,有任务的话会去检查是否有过期任务
    //有的话则添加到taskQueue中,并重新发起调度执行任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
复制代码

当currentTask不为null时,返回true,表示taskQueue中还有任务,需要继续调度。

当currentTask为null时,返回false,则表示taskQueue中所有任务都执行完成了,这时需要检查timeQueue中是否还有任务,有的话则需要在timeQueue中的第一个的任务过期时,将改任务添加值taskQueue中,并且由于此时上一个调度已经结束了,所以需要重新创建一个调度者发起任务调度:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;

  //检查延时任务队列中是否有已过期的任务
  //有的话则将过期任务拿出添加到过期任务队列中进行执行
  advanceTimers(currentTime);

  //isHostCallbackScheduled判断是否已经发起过调度
  //如果当前没有正在执行的调度,则会创建一个调度去执行任务
  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}
复制代码

我们在看向performWorkUntilDeadline函数,当workLoop执行完成时,会将返回值赋值给hasMoreWork

const performWorkUntilDeadline = () => {
    try {
      //调用scheduledHostCallback函数,开始任务的执行 scheduledHostCallback = flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      //hasMoreWork是workLoop函数最后返回的值
      //表示是否还有任务需要执行
      //如果为true表示有任务在执行中被中断,需要重新执行,那么则需要重新发起一个调度
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        //hasMoreWork为false表示taskQueue中的任务都执行完成了
        //需要将调度者释放,为下一次调度做准备
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
}
复制代码

hasMoreWorktrue时,则表示还有任务未执行,需要重新创建一个调度者,发起任务调度。

hasMoreWorkfalse时,则表示所有任务都执行完成了,将isMessageLoopRunningscheduledHostCallback重置,为下一次调度做好准备。

走到这里整个Scheduler的调度流程就结束了。

总结

Sheduler通过任务的优先级设置expirationTime过期时间,然后通过判断任务是否过期,分别将任务存放到timerQueue和taskQueue中。过期的任务都会从timerQueue中取出放到taskQueue中执行。

Scheduler在不同环境中创建调度者的方式不同:

  1. 服务端使用setImmediate来创建调度者
  2. 浏览器端使用MessageChannel来创建调度者
  3. 在以上方案都不能实现的时候,则降级使用setTimeout来实现创建调度者

为什么在不同环境要使用不同的API来创建调度者?

因为在服务端使用MessageChannel会阻止node进程的关闭,而是用setImmediate不会。

为什么不直接使用setTimeout来实现创建调度者的原因是因为setTimeout如果嵌套的层级超过了 5 层,并且 timeout 小于 4ms,则设置 timeout 为 4ms。而一帧的渲染时间为16.6ms,使用setTimeout默认就会使用4ms,这样的开销对于浏览器的是无法接受的。

任务的执行是使用While循环taskQueue依次取出任务执行,执行中会根据当前任务执行的时间是否超过一帧渲染的时间和用户是否与界面有交互来判断是否应该中断当前任务。如果任务被中断则会返回一个函数,任务未被中断则会返回null,以此返回值来判断任务的执行状态,选择是否需要恢复中断任务的执行。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享