定时器

timer 用于安排函数在未来某个时间点被调用,Node.js 中的定时器函数实现了与 Web 浏览器提供的定时器 API 类似的 API,但是使用了事件循环实现,Node.js 中有四个相关的方法:

  1. setTimeout(callback, delay[, …args])
  2. setInterval(callback[, …args])
  3. setImmediate(callback[, …args])
  4. process.nextTick(callback[, …args])
    1. // test.js
    2. setTimeout(() => console.log(1));
    3. setImmediate(() => console.log(2));
    4. process.nextTick(() => console.log(3));
    5. Promise.resolve().then(() => console.log(4));
    6. (() => console.log(5))();
    1. $ node test.js
    2. 5
    3. 3
    4. 4
    5. 1
    6. 2
    异步任务可以分成两种。
  • 追加在本轮循环的异步任务
  • 追加在次轮循环的异步任务

所谓”循环”,指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,
Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

事件循环

  1. ┌───────────────────────────┐
  2. ┌─>│ timers setTimeout/setImediate
  3. └─────────────┬─────────────┘
  4. ┌─────────────┴─────────────┐
  5. pending callbacks --
  6. └─────────────┬─────────────┘
  7. ┌─────────────┴─────────────┐
  8. idle, prepare --
  9. └─────────────┬─────────────┘ ┌───────────────┐
  10. ┌─────────────┴─────────────┐ incoming:
  11. poll │<─────┤ connections, 轮询
  12. └─────────────┬─────────────┘ data, etc.
  13. ┌─────────────┴─────────────┐ └───────────────┘
  14. check - setImmdiate
  15. └─────────────┬─────────────┘
  16. ┌─────────────┴─────────────┐
  17. └──┤ close callbacks - 关闭回调
  18. └───────────────────────────┘

注意:每个框被称为事件循环机制的⼀个阶段。 每个阶段都有⼀个 FIFO 队列来执行回调。

node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行微任务 process.nextTick() 等

然后进入事件循环

  • timers:此阶段执行由 setTimeout 和 setInterval 设置的回调。
  • pending callbacks:执行推迟到下一个循环迭代的 I/O 回调。

    此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。

  • idle, prepare, :仅在内部使用,可以忽略。

  • poll:轮询

    轮询阶段有两个重要的功能:

    1. 计算应该阻塞和轮询 I/O 的时间。
    2. 然后处理轮询队列里的事件。
  • check:在这里调用 setImmediate 回调。

  • close callbacks:一些关闭回调,例如 socket.on(‘close’, …)。

微任务在各个阶段之间执行

setImmediate

setImmediate() 和 setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

  • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本。
  • setTimeout插入的时机不一定,有一定的延时
  • 如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的
  • 一个 I/O 循环内调用,setImmediate 总是被优先调用。
  1. // 顺序不确定,只有两个语句,执行环境有差异
  2. // 场景1: setTimeout 0 最少1ms,未推入timers队列中,执行结果为:setImmediate、setTimeout
  3. // 场景2: setTimeout 0 已推入timers队列中,执行结果为:setTimeout、setImmediate
  4. setTimeout(()=>{
  5. console.log('setTimeout')
  6. }, 0)
  7. setImmediate(()=>{
  8. console.log('setImmediate')
  9. })
  10. //习题2: 都在回调函数中,内容确定
  11. //首轮事件循环setTimeout1的timers清空,执行至check阶段,先输出setImmediate
  12. //第二轮事件循环setTimeout2
  13. //最终输出:setTimeout1、setImmediate、setTimeout2
  14. setTimeout(()=>{
  15. setTimeout(()=>{
  16. console.log('setTimeout2')
  17. }, 0)
  18. setImmediate(()=>{
  19. console.log('setImmediate')
  20. })
  21. console.log('setTimeout1')
  22. }, 0)

微任务调整

  • node11 开始,每执行完一个timer类回调,例如 setTimeout, setImmediate 之后,都会把微任务给执行掉(promise等)
  • node10和以前: 当一个任务队列(例如timer queue)里面的回调都批量执行完了,才去执行微任务
  1. async function async1(){
  2. console.log('async1 started');
  3. await async2();
  4. console.log('async end');
  5. }
  6. async function async2(){
  7. console.log('async2');
  8. }
  9. console.log('script start.');
  10. setTimeout(() => {
  11. console.log('setTimeout0');
  12. setTimeout(() => {
  13. console.log('setTimeout1');
  14. }, 0);
  15. setImmediate(()=>{
  16. console.log('setImmediate');
  17. })
  18. }, 0);
  19. process.nextTick(() => {
  20. console.log('nextTick');
  21. })
  22. async1();
  23. new Promise(()=>{
  24. console.log('promise1');
  25. resolve();
  26. console.log('promise2');
  27. }).then(() =>{
  28. console.log('promise.then')
  29. });
  30. console.log('script end.');

参考文章