对 Nodejs 事件循环中的计时器和微任务感到困惑

Confused about timers and microtasks in Nodejs event loop

我对下面代码的输出感到困惑。 (摘自 IBM 开发人员 Nodejs 课程)。

'use strict'

const logger = require('../common/logger');

const { MAINLINE, START, END } = require('../common/constants');

(function mainline() {
    logger.info(START, MAINLINE);
    
    process.nextTick(() => {
        logger.info('mainline:process.nextTick() says: hello!', 'MICROTASK')
    });

    let iteration = 0;
    let intervalTimeout = setInterval(() => {
        if (iteration < 3) {
            setTimeout((iteration) => {
                logger.info('setInterval(' + iteration + '):setTimeout() says: Timer expired!', 'TIMERS');
                process.nextTick((iteration) => {
                    logger.info('setInterval():setTimeout(' + iteration + '):process.nextTick() says: Delimit TIMERS phase!', 'MICROTASK');
                }, iteration);
            }, 0, iteration);
        } else {
            logger.info('setInterval(' + iteration + ') says: Goodbye!', 'TIMERS');
            clearInterval(intervalTimeout);
        }
        iteration++;
    });

    logger.info(END, MAINLINE)
})();

输出结果如下:

1593574413204:INFO: MAINLINE: START
1593574413206:INFO: MAINLINE: END
1593574413207:INFO: MICROTASK: mainline:process.nextTick() says: hello!
1593574413210:INFO: TIMERS: setInterval(0):setTimeout() says: Timer expired!
1593574413210:INFO: MICROTASK: setInterval():setTimeout(0):process.nextTick() says: Delimit TIMERS phase!
1593574413211:INFO: TIMERS: setInterval(1):setTimeout() says: Timer expired!
1593574413211:INFO: MICROTASK: setInterval():setTimeout(1):process.nextTick() says: Delimit TIMERS phase!
1593574413213:INFO: TIMERS: setInterval(2):setTimeout() says: Timer expired!
1593574413213:INFO: TIMERS: setInterval(3) says: Goodbye!
1593574413213:INFO: MICROTASK: setInterval():setTimeout(2):process.nextTick() says: Delimit TIMERS phase!

为什么记录器在最后一个计时器微任务之前记录“再见”?

如果您使用递归的 setTimeout 而不是 setInterval 重写此代码,也许会使发生的事情更清楚。

那会给出类似

的东西
(function mainline() {

  let iteration = 0;
  function intervalJob() {
    if (iteration < 3) {
      setTimeout(timeoutJob, 0, interation);
      // reproduce what setInterval does using recursive setTimeout
      setTimeout(intervalJob, 0);
    }
    else {
      logger.info('setInterval(' + iteration + ') says: Goodbye!', 'TIMERS');
    }
    iteration++;
  }
  function timeoutJob(iteration) {
    logger.info('setInterval(' + iteration + '):setTimeout() says: Timer expired!', 'TIMERS');
    process.nextTick(microtaskJobFromTimeout, iteration);
  }
  function microtaskJobFromTimeout(iteration) {
    logger.info('setInterval():setTimeout(' + iteration + '):process.nextTick() says: Delimit TIMERS phase!', 'MICROTASK');
  }

  logger.info("START", "MAINLINE");
  
  process.nextTick(() => {
    console.log('mainline:process.nextTick() says: hello!', 'MICROTASK')
  });
  
  setTimeout(intervalJob,0);
  
  logger.info("END", "MAINLINE")

})();

基本上,在 iteration 到达 3 之前,每个 intervalJob 调用将安排 两个 新作业在下一个事件循环中执行迭代:timeoutJob,它本身会在内部调度一个微任务,递归 intervalJob.

因为节点的事件循环首先执行所有超时作业,然后仅执行所有微任务,最后一个 intervalJob 实际上将在与最后一个 timeoutJob 相同的事件循环迭代期间位于超时池中.

所以timeoutJob之前通常会执行,因为它首先被调度,然后intervalJob因为它也在定时器池中,最后是微任务,在循环重复之前。

您可以通过从 setInterval 回调中无条件记录来验证这一点,输出将是:

MAINLINE: START
MAINLINE: END
MICROTASK: mainline:process.nextTick() says: hello!
INTERVALJOB: unconditional intervalJob logging
TIMERS: setInterval(0):setTimeout() says: Timer expired!
INTERVALJOB: unconditional intervalJob logging
MICROTASK: setInterval():setTimeout(0):process.nextTick() says: Delimit TIMERS phase!
TIMERS: setInterval(1):setTimeout() says: Timer expired!
INTERVALJOB: unconditional intervalJob logging
MICROTASK: setInterval():setTimeout(1):process.nextTick() says: Delimit TIMERS phase!
TIMERS: setInterval(2):setTimeout() says: Timer expired!
TIMERS: setInterval(3) says: Goodbye!
INTERVALJOB: unconditional intervalJob logging
MICROTASK: setInterval():setTimeout(2):process.nextTick() says: Delimit TIMERS phase!

请注意,这是节点特定的行为,在浏览器中,每个任务都有自己的微任务检查点,并且计划同时触发的两个计时器实际上将在两个不同的事件循环迭代中执行.