深入理解 setTimeout、setImmediate、process.nextTick
发布于 2 年前 作者 OneNewLife 4334 次浏览 来自 分享

setTimeout 注册的回调会在事件循环的 timerspollclosing callbacks 阶段执行。需要注意的是,计时器默认定义的 TIMEOUT_MAX 的取值范围是 [1, 2 ^ 31 - 1],不足 1 或者超过上限都会初始化为 1,也就是说你调用 setTimeout(fn, 0)setTimeout(fn, 1) 的效果是一样的。

process.nextTick 注册的回调会在事件循环的当前阶段结束前执行,而不是只有 pollcheck 阶段才会执行。process 是内核模块,运行时是全局上下文,所以 microtask 只有一个,无论你是在哪个阶段、哪个闭包内用 nextTick 注册的回调都会被 pushnextTickQueue,并在事件循环当前阶段结束前执行。

setImmediate 注册的回调会在 check 阶段、check 阶段、check 阶段执行。因为它需要由 check watcher 来执行,check watcher 只在 check 阶段处于 active 状态。与 process.nextTick 不同,setImmediate 因运行时的上下文不同而产生不同的 ImmediateList,所以 macrotask 可以有多个。setImmediate 会在异常的时候执行 process.nextTick(processImmediate),会在当前阶段结束前重新执行一次这个异常任务(即 check 阶段)。

具体的执行过程参考 node/deps/uv/src/unix/core.c

// 332
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

具体执行情况参见下面这个例子:

/**
 * 执行栈中注册 setTimeout 计时器
 */
setTimeout(function () {
  // 4. timers 阶段。timer watcher 遍历计时器 Map 的 key,
  // 如果有 key <= timeout,执行该 key 对应的 value(计时器任务);
  // 否则等到 poll 阶段再检查一次
  console.log('setTimeout');

  setTimeout(function () {
    // 11. 注册 setTimeout 计时器。UV_RUN_ONCE 模式下,
    // 会在循环结束之前再执行时间下限到达的计时器任务,取决于进程性能
    // 1 <= TIMEOUT_MAX <= 2 ^ 31 - 1
    console.log('setTimeout in setTimeout');
  }, 0);

  setImmediate(function () {
    // 9. 注册 setImmediate 计时器。在当前循环的 check 阶段执行。
    // (注:这是新的 ImmediateList,当前循环内有 3 个 ImmediateList 了)
    console.log('setImmediate in setTimeout');
  });
  
  process.nextTick(function () {
    // 6. 为 nextTickQueue 添加任务,timers 阶段结束前唤醒 idle watcher
    // idle watcher 检查 nextTickQueue,执行任务
    console.log('nextTick in setTimeout');
  });
}, 0);

/**
 * 执行栈中注册 setImmediate 计时器
 */
setImmediate(function () {
  // 7. poll 阶段没有可执行任务,阶段结束前唤醒 idle watcher,idle watcher 继续睡;
  // 接着唤醒 check watcher,检测到 ImmediateList 不为空,进入 check 阶段。
  // check watcher 执行第一个任务 
  console.log('setImmediate');

  setTimeout(function () {
    // 13. 注册 setTimeout 计时器
    // 由于机器性能,在循环结束前才执行
    console.log('setTimeout in setImmediate');
  }, 0);

  setImmediate(function () {
    // 12. 为当前 ImmediateList 添加任务
    // 由于机器性能优越,前面 nextTickQueue 为空了,直接进入 check 阶段
    console.log('setImmediate in setImmediate');
  });
  
  process.nextTick(function () {
    // 10. 为 nextTickQueue 添加任务,当所有 ImmediateList 的队首任务都执行完毕时,
    // 唤醒 idle watcher,检查 nextTickQueue,执行队列任务
    console.log('nextTick in setImmediate');
  });
});

/**
 * 执行栈中为 nextTickQueue 添加任务
 */
process.nextTick(function () {
  // 2. 执行栈为空,进入事件循环准备阶段,唤醒 prepare watcher,
  // 检查 nextTickQueue,执行队列中的任务
  console.log('nextTick');

  setTimeout(function () {
    // 5. 注册计时器任务,timers 阶段到达时间下限则执行该任务,
    // 否则等到 poll 阶段
    console.log('setTimeout in nextTick');
  }, 0);

  setImmediate(function () {
    // 8. 注册 setImmediate 计时器,在当前循环的 check 阶段执行。
    // (注:这是新的 ImmediateList,当前循环内有 2 个 ImmediateList 了)
    console.log('setImmediate in nextTick');
  });
  
  process.nextTick(function () {
    // 3. prepare watcher 处于活跃状态,检测 nextTickQueue 的新任务,
    // 执行完所有任务后沉睡
    console.log('nextTick in nextTick');
  });
});

console.log('main thread'); // 1. 执行栈的任务

// 输出:
// main thread
// nextTick
// nextTick in nextTick
// setTimeout
// setTimeout in nextTick
// nextTick in setTimeout
// setImmediate
// setImmediate in nextTick
// setImmediate in setTimeout
// nextTick in setImmediate
// setTimeout in setTimeout
// setImmediate in setImmediate
// setTimeout in setImmediate
/* 后面 setImmediate 注册的回调会因为进程执行性能顺序有所不同 */

补充: 多谢 @hyj1991 的提醒,前面忽略了 microtask 的一些细节没有说清楚。

我们在日常应用中经常会将 promise、process.nextTick、nextTickQueue、microtask 混为一谈,其实真正注册为 microtask 的任务的目前只有 promise。但是问题来了,v8 目前是没有暴露 runMicrotasks ,也就是说我们目前还没有办法通过内核的 API 执行 microtask queue 的任务。Node.js 最终选择的实现方法是将 microtask queue 的任务通过一个 runMicrotasks 对象暴露给上游,然后通过 nextTick 方法把它们推进了 nextTickQueue,也就是说最终 microtask queue 的任务变成了 nextTickQueue 的任务,所以我们用 promise.then 和 process.nextTick 可以实现相同的效果。

具体实现参考 node/lib/internal/next_tick.js

function setupNextTick() {
  const promises = require('internal/process/promises');
  const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks);
  var nextTickQueue = [];
  var microtasksScheduled = false;

  // Used to run V8's micro task queue.
  var _runMicrotasks = {};
  
  function scheduleMicrotasks() {
    if (microtasksScheduled)
      return;

    nextTickQueue.push({
      callback: runMicrotasksCallback,
      domain: null
    });

    tickInfo[kLength]++;
    microtasksScheduled = true;
  }

  function runMicrotasksCallback() {
    microtasksScheduled = false;
    _runMicrotasks();

    if (tickInfo[kIndex] < tickInfo[kLength] ||
        emitPendingUnhandledRejections())
      scheduleMicrotasks();
  }
}
7 回复

@JacksonTian 谢 piao 大大

我觉得要讲确切一些,node的process.nextTick只是在设计上非常接近microtask部分,实际上nextTickQueue完全是js层构造的东西,和v8提供的microtask queue没有任何关系。真正注册到v8 microtask queue和使用 isolate->RunMicrotasks() 驱动的只有ES6中的 promise

@hyj1991 多谢提醒,差点忽略了 microtasksScheduled

@OneNewLife process.nextTick(fn) fn不应该是事件队列下次循环执行吗 ? 事件循环一次为一个Tick,如果在当前事件队列循环结束前执行,那么nextTick这个名字取的意义也不对啊!

@danielsss nextTick 和 setImmediate 它们俩做的事情和名字是相反的,node 文档里有提到

回到顶部