likes
comments
collection
share

面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭

作者站长头像
站长
· 阅读数 38

在一次面试时,面试官问我 react scheduler 调度机制原理是什么?我却支支吾吾想了半天没答上来,看着面试官轻蔑的表情,我在心里默默的下定决心:三十年河东三十年河西,莫欺少年穷......

面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭

什么是 scheduler?

scheduler​(协调器)是react的更新流程中非常重要的一环。只需要将任务和任务的优先级交给它,它就可以帮你管理任务,安排任务的执行。

在涉及到例如大量的dom更新操作,如果一直同步执行这个耗时非常久的任务,就会一直占用着线程,所以就会造成用户在使用浏览器时视觉上的卡顿。

scheduler​对于更新的方式上做出优化:对于单个任务来说,会有节制地去执行,不会一直占用着线程去执行任务。而是执行一会,中断一下,再执行,一直重复。而对于多个任务,它会先执行高优先级任务。

对于scheduler​的执行特性,可以看出来主要是对两种形式进行优化:多个任务之间的管理和单个任务的执行控制。这也就引申出来scheduler​两种概念:时间片、任务优先级

时间片与优先级

时间片是指在单个任务在这一帧内最大的执行时间,超过这个时间后会立即被打断,不会一直占用线程,这样页面就不会因为任务连续执行的时间过长而产生视觉上的卡顿。

优先级是指在有多个任务待执行时,按照优先级的顺序依次执行,这样可以使一些紧急任务先被执行。

既然存在着优先级的概念。那么必然存在一个任务队列对所有的任务进行管理,按照某种顺序对所有的任务进行排序。在scheduler​中存在着两种队列,分别对不同的任务进行管理。

优先级

在探究调度函数是如何加入到队列中之前,先来看一下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​接收一个任务后,会为这个任务创建一个调度对象,对象中保存这个任务的优先级,开始时间,过期时间以及真实的执行函数:

  const newTask = {
    id: 0,
    // 任务函数
    callback,
    // 任务优先级
    priorityLevel,
    // 开始时间
    startTime,
    // 过期时间
    expirationTime,
    // 在队列中排序的依据
    sortIndex: -1,
  };
  • sortIndex​:在队列中的排序依据,在scheduler​中使用小顶堆的形式来构建队列
  • callback​:真正的任务函数,也就是外部传入的任务函数
  • priorityLevel​:任务优先级,参与计算任务过期时间
  • startTime​:表示任务开始的时间
  • expirationTime​:表示任务的过期时间

callback​保存真正需要执行的任务函数。priorityLevel​为上文中依据不同的触发类型划分的优先级。startTime​在生成调度对象时被初始化为当前时间:

// getCurrentTime函数获取当前时间
const currentTime = getCurrentTime();

但是过期时间expirationTime​是怎么计算出来的呢?一旦过期时间小于当前时间,则说明当前的任务需要立即被执行,在scheduler​中计算过期时间与优先级密切相关。

不同优先级都会对应的不同的任务过期时间间隔:

var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
ImmediatePriority --> IMMEDIATE_PRIORITY_TIMEOUT --> -1
UserBlockingPriority --> USER_BLOCKING_PRIORITY_TIMEOUT --> 250
NormalPriority --> NORMAL_PRIORITY_TIMEOUT --> 5000
LowPriority --> LOW_PRIORITY_TIMEOUT --> 10000
IdlePriority --> IDLE_PRIORITY_TIMEOUT --> maxSigned31BitInt

而过期时间的计算,则是任务开始时间加上优先级代表的时间间隔:

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;
}

// startTime看作任务开始时间
var expirationTime = startTime + timeout;

ImmediatePriority​ 的执行间隔为-1,在计算过期时间时当前时间与-1相加,也就是说ImmediatePriority​优先级的任务过期时间肯定会小于任务开始时间。

当调度对象被确定后,会把任务分成了两种:未过期的和已过期的。分别用两个队列存储,前者存到timerQueue​中,后者存到taskQueue​中。

判断任务是否过期使用任务的开始时间startTime​和当前时间currentTime​(当前时间)作比较。开始时间大于当前时间,说明未过期,放到timerQueue​队列;开始时间小于等于当前时间,说明已过期,放到taskQueue​队列。

当任务被放入不同的队列时,两个队列需要按照一定的规则来排序:

  • timerQueue​队列按照startTime​开始时间排序,开始时间越小越靠前。因为开始时间越早,说明会越早开始。任务进来的时候,开始时间默认是当前时间,如果进入调度的时候传了延迟时间,开始时间则是当前时间与延迟时间的和。
  • taskQueue​队列按照expirationTime​过期时间排序。过期时间越早,说明越紧急,过期时间小的排在前面。

如果任务被放到了taskQueue​队列,那么立即调度一个函数去循环taskQueue​,挨个执行里面的任务。

如果任务被放到了timerQueue​队列,那么说明它里面的任务都不会立即执行。等待排在第一位的任务间隔时间到了之后,将第一个任务加入到taskQueue​队列中。然后重复执行这个动作,直到timerQueue​队列中的任务被清空。

面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭

任务队列的核心就是保证最紧急的任务优先被调度执行。

scheduleCallback

面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭

scheduleCallback​函数是整个调度的入口函数。主要负责生成调度任务、根据任务是否过期将任务放入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; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
      break;
  }
  // 计算任务的过期时间,任务开始时间 + timeout
  // 若是立即执行的优先级(ImmediatePriority)过期时间是startTime - 1,意味着立刻就过期
  var expirationTime = startTime + timeout;

  // 创建调度任务
  var newTask = {
    id: taskIdCounter++,
    // 真正的任务函数
    callback,
    // 任务优先级
    priorityLevel,
    // 任务开始的时间,表示任务何时才能执行
    startTime,
    // 任务的过期时间
    expirationTime,
    // 在小顶堆队列中排序的依据
    sortIndex: -1,
  };

  // 如果任务已过期,则将 newTask 放入taskQueue,调用requestHostCallback函数
  // 开始调度执行taskQueue中的任务
  if (startTime > currentTime) {
    // 任务未过期,以开始时间作为timerQueue排序的依据
    newTask.sortIndex = startTime;
	// 加入timerQueue队列
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 如果现在taskQueue中没有任务,并且当前的任务是timerQueue中排名最靠前的那一个
      // 那么需要检查timerQueue中有没有需要放到taskQueue中的任务
      if (isHostTimeoutScheduled) {
        // 因为即将调度一个requestHostTimeout,所以如果之前已经调度了,那么取消掉
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 调用requestHostTimeout实现任务的转移,开启调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 任务已经过期,以过期时间作为taskQueue排序的依据
    newTask.sortIndex = expirationTime;
	// 加入taskQueue队列
    push(taskQueue, newTask);

    // 开始执行任务,使用flushWork去执行taskQueue
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    }
  }

  return newTask;
}

首先会为传入的任务计算开始时间,支持用户自定义延迟时间,代表多久之后开始执行,如果没有延迟时间,则将当前时间作为开始时间,代表立即开始执行。

然后根据优先级计算任务的过期时间,若是立即执行的优先级(ImmediatePriority​)过期时间是startTime - 1​,意味着立刻就过期。

根据优先级,开始/过期时间构建调度对象,其中callback​保存真正的任务函数。

根据开始时间为调度函数分配任务队列:

  • 开始时间小于当前时间:代表当前任务需要开始调度执行,将它放入taskQueue​​队列中,调用requestHostCallback​​函数,让调度者调度一个执行函数去执行任务,也就意味着调度流程开始。
  • 开始时间大于当前时间:代表任务还不需要开始调度执行,会将任务放入timerQueue​队列,并按照开始时间排列,然后调用requestHostTimeout​函数开启一个定时器,设置时间间隔为第一个timerQueue​队列任务的开始时间,再去检查它是否已经到达开始时间,如果到达开始时间startTime​那么加入taskQueue​队列。这个过程通过handleTimeout​函数处理。

requestHostTimeout​ 函数开启一个定时器。

// 开始一个定时器
function requestHostTimeout(callback, ms,) {
  localSetTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

handleTimeout​的职责是:

  • 调用advanceTimers​,检查timerQueue​队列中过期的任务,放到taskQueue​中。

  • 检查是否已经开始调度,如尚未调度,检查taskQueue​中是否已经有任务:

    • 如果有,而且现在是空闲的,说明之前的advanceTimers已经将过期任务放到了taskQueue,那么现在立即开始调度,执行任务
    • 如果没有,而且现在是空闲的,说明之前的advanceTimers​并没有检查到timerQueue​中有过期任务,那么再次调用requestHostTimeout​重复这一过程。
function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // 切换任务队列
  advanceTimers(currentTime);

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

其中pop​以及peek​和push​都是操作队列的方法,pop​为队列第一项被弹出(类似于数组的pop()​方法)。

peek​为获取队列的第一项,与pop​方法的区别是,peek​只是获取,不会删除,类似于数组的arr[0]​。

push​为向队列尾部加入一项,类似于数组的push()​方法。

function advanceTimers(currentTime) {
  let timer = peek(timerQueue);

  while (timer !== null) {
    if (timer.callback === null) {
      pop(timerQueue);
	// 任务需要开始了,去taskQueue队列吧
    } else if (timer.startTime <= currentTime) {
      pop(timerQueue);
	  // 将sortIndex变更为过期时间
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
    } else {
      return;
    }
    timer = peek(timerQueue);
  }
}

requestHostCallback

requestHostCallback​开始正式进入一个任务调度执行的过程了。上文中调用requestHostCallback​函数是这样被调用的:

// 开始执行任务,使用flushWork去执行taskQueue
if (!isHostCallbackScheduled && !isPerformingWork) {
     isHostCallbackScheduled = true;
     requestHostCallback();
}

首先将isHostCallbackScheduled​ 设置为true​,表示当前正在调度一个任务。然后将flushWork​传入requestHostCallback​函数开始执行,那么这个flushWork​是什么呢?

既然flushWork​是入参,那么flushWork​必然是实际执行任务的函数,而requestHostCallback​对任务调度进行调度。

let schedulePerformWorkUntilDeadline;

// node/ie
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
// 浏览器环境 MessageChannel
} else if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
// 其他
} else {
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}


function requestHostCallback() {
  // 当前是否有正在执行的任务?
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

在不同的宿主环境调用方式是不同的:

  • 非浏览器环境,使用定时器执行,因为在非浏览器环境中,用户是看不到界面的,也不存在用户交互,所以卡顿与否也就不那么重要了。
  • 浏览器中使用MessageChannel​实现

schedulePerformWorkUntilDeadline​在不同的宿主环境会被赋予不同的实现方式,我们目前只关注浏览器中的实现方式,先来简单了解一下MessageChannel​是怎么使用的:

MessageChannel

MessageChannel​ 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort​ 属性发送数据。

同时允许我们在不同的浏览上下文,比如window.open()​打开的窗口或者iframe等之间建立通信管道,并通过两端的端口(port1​和port2​)发送消息。MessageChannel​以DOM Event​的形式发送消息,所以它属于异步的宏任务。

port1​订阅一个消息,当port2​发送一个消息时,port1​执行订阅的函数并且接收到了port2​发送的消息。

const { port1, port2 } = new MessageChannel();
port1.onmessage = function (event) {
  console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
};

port2.postMessage('pong');

回到我们scheduler​的执行过程,在浏览器中schedulePerformWorkUntilDeadline​被赋予了触发postMessage​的能力,而接收者port1​订阅了执行函数performWorkUntilDeadline​,所以在schedulePerformWorkUntilDeadline​函数执行的时候相当于port2​通过消息通道发送了一条消息,port1​执行performWorkUntilDeadline​函数,也就是真正任务的执行在performWorkUntilDeadline​函数中。这个过程中会涉及任务的中断和恢复任务完成状态的判断。

performWorkUntilDeadline

performWorkUntilDeadline​函数的职责是按照时间片的限制去中断任务,并通知调度者再次调度一个新的执行者去继续任务。直到任务被完全执行完。 ‍

// 记录开始时间
let startTime = -1;

const performWorkUntilDeadline = () => {
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
	// 注意此时将任务开始时的当前时间记录下来
	// 后面切分时间片使用
    startTime = currentTime;

    let hasMoreWork = true;
    try {
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
		// 如果还有任务,继续让调度者调度执行者
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
};

hasMoreWork​函数保存本次执行的执行结果,是否继续调度任务依赖于flushWork​的执行结果

flushWork​作为真正去执行任务的函数,它会循环taskQueue​队列,逐一调用里面的任务函数。

function flushWork(initialTime: number) {
  // ...省略
  return workLoop(initialTime);
}

flushWork​函数中将workLoop​函数的执行结果return​出去,任务执行的核心内容看来就在workLoop​中。workLoop​的调用使得任务最终被执行。

workLoop​函数中,首先就会涉及到任务的中断和恢复。

任务的中断和恢复

scheduler​根据时间片限制任务的执行时间,既然任务会被强制中断,那么也就必然会恢复执行。

先来看一下workLoop​大体的执行流程:

function workLoop(initialTime) {
  let currentTime = initialTime;
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      // 中断任务
      break;
    }

    const callback = currentTask.callback;
    if (typeof callback === 'function') {
  
		// ...省略代码
		// 执行任务

    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }

  if (currentTask !== null) {
 	// 如果currentTask不为空,说明是时间片的限制导致了任务中断
    // return 一个 true告诉外部,此时任务还未执行完,任务是被中断
    return true;
  } else {
	// 如果currentTask为空,说明taskQueue队列中的任务已经执行完
	// 开始查找timerQueue队列中是否还有任务
    const firstTimer = peek(timerQueue);
	// 如果timerQueue队列中还有任务,创建一个定时器
	// 等待到达任务的开始时间后放入taskQueue队列
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }

	// return false 告诉外部当前的taskQueue队列已经被清空
    return false;
  }

可以看到,整个workLoop​函数分为两个部分去执行任务:任务执行和任务状态的判断。

taskQueue循环

workLoop​开始执行时,使while​循环执行任务,在执行的过程中可以看到时间片的中断条件:

在任务的过期时间大于当前时间时(说明任务还未过期),并且shouldYieldToHost​函数true​。

if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
     // 中断任务
     break;
}

shouldYieldToHost​函数用户在任务执行时切分时间片,当任务时间超过规定的时间时,强制中断:

break​的只是while​循环,while​下部还是会判断currentTask​的状态。

export const frameYieldMs = 5;
let frameInterval = frameYieldMs;

function shouldYieldToHost(): boolean {
  // 计算时间间隔
  const timeElapsed = getCurrentTime() - startTime;
  // 间隔时间大于5毫秒,中断执行
  if (timeElapsed < frameInterval) {
    return false;
  }
  return true
}

performWorkUntilDeadline​在开始任务执行的时候记录开始时间,在任务每次执行过程中(while​循环),判断任务开始的时间与当前时间是否大于5ms,以此对任务进行切分。

currentTask​正常执行,可是时间不允许,那只能先break​掉本次while​循环,使得本次循环currentTask​执行的逻辑都不能被执行到。

由于任务只是被中止,所以currentTask​不可能是null​,那么会return​一个true​告诉外部任务并没有执行结束,只是被暂时中断。

否则说明全部的任务都已经执行完了,taskQueue​ 任务队列已经被清空了,此时return​一个false​让外部终止本次调度

由于workLoop​的执行结果会被flushWorkreturn​出去,所以在performWorkUntilDeadline​函数中整个执行流程就会非常清晰:

const performWorkUntilDeadline = () => {
  if (isMessageLoopRunning) {
	// 获取当前时间
    const currentTime = getCurrentTime();
    startTime = currentTime;

    let hasMoreWork = true;
    try {
	  // 当任务因为时间片被打断时,它会返回true,表示还有任务
      hasMoreWork = flushWork(currentTime);
    } finally {
	  // hasMoreWork为true,代表人物只是被中断了,并没有执行完
      if (hasMoreWork) {
		// 调度一个调度者,继续去执行完任务
        schedulePerformWorkUntilDeadline();
      } else {
		// 如果没有任务了,停止调度
        isMessageLoopRunning = false;
      }
    }
  }
  needsPaint = false;
};

当任务被打断之后,performWorkUntilDeadline​会再让调度者调用一个执行者,继续执行这个任务,直到任务完成。

由于任务被中断只是循环被break​,当前被中断的任务还依然保存在taskQueue​队列中的第一个并未被弹出,所以下次调度执行的任务依然是上次被中断的那一个。

所以整个任务的执行流程:

调度任务 --> 执行任务 --> 打断 --> 调度任务 --> 执行任务 ... --> 完成

既然中断和恢复任务的流程已经明白是如何运行的了,那么如何判断该任务是否完成呢?

如何判断任务的完成状态?

任务的中断恢复是一个重复的过程,该过程会一直重复到任务完成。所以判断任务是否完成非常重要,而任务未完成则会重复执行任务函数

由于scheduler​是完全独立的一个包,调用的时候将需要调度的任务交给它,由它来调度执行,但是scheduler​在调度的时候只是关注于去执行任务,触及不到具体的任务的执行逻辑,所以在调度的时候它是无法准确的得知任务是什么时候结束的,它可以做到重复执行任务函数,但边界(即任务是否完成)却无法像递归那样直接获取,只能依赖任务函数的返回值去判断。

所以若执行的任务函数返回值为函数,那么就说明当前任务尚未完成,需要继续调用任务函数,否则任务完成workLoop​就是通过判断返回值的方式判断单个任务的完成状态

(以下实现有删减):

function workLoop(initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null
  ) {
	// 中断任务条件
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      break;
    }

    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      // 任务的优先级
      currentPriorityLevel = currentTask.priorityLevel;
	  // 任务是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
 	  // 执行任务
	  // 获取返回值
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
	  // 如果任务只是被中断了,返回值为一个函数,任务函数自身
      if (typeof continuationCallback === 'function') {
 		// 检查callback的执行结果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。
        currentTask.callback = continuationCallback;
        return true;
      } else {
		// 如果返回值不是函数,说明任务已经彻底执行完毕
        if (currentTask === peek(taskQueue)) {
		  // 在taskQueue队列中弹出当前任务
          pop(taskQueue);
        }
      }
	  // 查找timerQueue队列中是否有可以开始执行的任务
	  advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // 返回当前任务是否被执行完的标记
  // 如果执行完 (else逻辑)则需要去timerQueue中查看是否有需要加入到taskQueue的任务
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

所以,workLoop是通过判断任务函数的返回值去识别任务的完成状态的

开始调度后,调度者调度执行者去执行任务,实际上是执行任务上的callback​(也就是任务函数)。如果执行者判断callback​返回值为一个function​,说明任务并没有完成,只是暂时被中断。

那么会将返回的这个function​再次赋值给任务的callback​,由于任务还未完成,所以并不会被弹出出taskQueue​队列,currentTask​获取到的还是这个任务,while​循环到下一次还是会继续执行这个任务,直到任务完成弹出队列,才会继续调度下一个任务。

取消任务

取消任务的场景是如果当前正在执行一个比较低优先级的任务,这是有一个高优先级的任务进来了,所以就需要先将正在执行的低优先级的任务取消掉,然后调度一个任务去优先执行高优先级的任务。

workLoop​中获取当前调度对象的callback​属性,如果是函数就开始执行,如果不是的话直接将这个任务弹出taskQueue​队列。

function workLoop(initialTime) {
  ...

  // 获取taskQueue中最紧急的任务
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    ...
    const callback = currentTask.callback;

    if (typeof callback === 'function') {
      // 执行任务
    } else {
      // 如果callback为null,将任务出队
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  ...
}

所以取消任务直接将callback​属性赋值为null​就可以了。

function unstable_cancelCallback(task) {
  // ...省略
  task.callback = null;
}

workLoop​中,如果callback​是null​会被移出taskQueue​队列,所以当前的这个任务就不会再被执行了。它取消的是当前任务的执行,while​循环还会继续执行下一个任务。

Demo

最后实现一个依靠recheduler来调度任务生成数据的小案例:

import {
	unstable_ImmediatePriority as ImmediatePriority,
	unstable_UserBlockingPriority as UserBlockingPriority,
	unstable_NormalPriority as NormalPriority,
	unstable_LowPriority as LowPriority,
	unstable_IdlePriority as IdlePriority,
	unstable_scheduleCallback as scheduleCallback,
	unstable_shouldYield as shouldYield,
	CallbackNode,
	unstable_getFirstCallbackNode as getFirstCallbackNode,
	unstable_cancelCallback as cancelCallback
} from 'scheduler';

import './style.css';
const button = document.querySelector('button');
const root = document.querySelector('#root');

type Priority =
	| typeof IdlePriority
	| typeof LowPriority
	| typeof NormalPriority
	| typeof UserBlockingPriority
	| typeof ImmediatePriority;

interface Work {
	count: number;
	priority: Priority;
}

const workList: Work[] = [];
let prevPriority: Priority = IdlePriority;
let curCallback: CallbackNode | null = null;

[LowPriority, NormalPriority, UserBlockingPriority, ImmediatePriority].forEach(
	(priority) => {
		const btn = document.createElement('button');
		root?.appendChild(btn);
		btn.innerText = [
			'',
			'ImmediatePriority',
			'UserBlockingPriority',
			'NormalPriority',
			'LowPriority'
		][priority];
		btn.onclick = () => {
			workList.unshift({
				count: 100,
				priority: priority as Priority
			});
			schedule();
		};
	}
);

function schedule() {
	const cbNode = getFirstCallbackNode();
	const curWork = workList.sort((w1, w2) => w1.priority - w2.priority)[0];

	// 策略逻辑
	if (!curWork) {
		curCallback = null;
		cbNode && cancelCallback(cbNode);
		return;
	}

	const { priority: curPriority } = curWork;
	if (curPriority === prevPriority) {
		return;
	}
	// 更高优先级的work
	cbNode && cancelCallback(cbNode);

	curCallback = scheduleCallback(curPriority, perform.bind(null, curWork));
}

function perform(work: Work, didTimeout?: boolean) {
	/**
	 * 1. work.priority
	 * 3. 时间切片
	 */
	const needSync = work.priority === ImmediatePriority || didTimeout;
	while ((needSync || !shouldYield()) && work.count) {
		work.count--;
		insertSpan(work.priority + '');
	}

	// 中断执行 || 执行完
	prevPriority = work.priority;

	if (!work.count) {
		const workIndex = workList.indexOf(work);
		workList.splice(workIndex, 1);
		prevPriority = IdlePriority;
	}

	const prevCallback = curCallback;
	schedule();
	const newCallback = curCallback;

	if (newCallback && prevCallback === newCallback) {
		return perform.bind(null, work);
	}
}

function insertSpan(content) {
	const span = document.createElement('span');
	span.innerText = content;
	span.className = `pri-${content}`;
	doSomeBuzyWork(10000000);
	root?.appendChild(span);
}

function doSomeBuzyWork(len: number) {
	let result = 0;
	while (len--) {
		result += len;
	}
}

那么扯了这么多,等下次面试再遇到这个问题怎么回答呢?

总结

scheduler​管理taskQueue​和timerQueue​两个队列,定期将timerQueue​中的到达开始时间的任务放到taskQueue​中,然后让调度者通知执行者循环taskQueue​执行每一个任务。

执行者控制着每个任务的执行,一旦某个任务的执行时间超出时间片的限制。就会被中断,然后当前的执行者退出,退出之前会通知调度者再去调度一个新的执行者继续完成这个任务,新的执行者在执行任务时依旧会根据时间片中断任务,然后退出,重复这一过程,直到当前这个任务彻底完成后,将任务从taskQueue​​出队。

taskQueue​​中每一个任务都被这样处理,最终完成所有任务,这就是scheduler​​的完整工作流程。

写在最后 ⛳

未来可能会更新实现mini-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳 ‍

转载自:https://juejin.cn/post/7331135154209308687
评论
请登录