Delay a JavaScript function call

echosoar 原创发表于 2019/10/30 10:13:32
#event loop #nodejs
在Node中要实现延时一个任务执行有很多种方法:
  • setTimeout(callback, 0)
  • setImmediate(callback, 0)
  • process.nextTick
  • promise.resolve
此篇文章来分析一下这些方法有什么区别。
Node中的任务分为宏任务(MacroTask)和微任务(MicroTask),在每一个宏任务阶段中间穿插着微任务。 宏任务包含:script全部代码、setTimeout、setInterval、setImmediate、I/O等。 微任务包含:Process.nextTick、Promise等。
由于Node的Event Loop是基于libuv实现的,首先来看一下libuv这一张完整的事件循环阶段图:
执行的阶段如下:
  1. 已经到期的timer,也就是setTimeoutsetIntervel
  2. 大多数情况下,在轮询I/O之后立即调用所有I/O回调。但在某些情况下,有些回调会推迟到下一个循环,也就是在这里执行上一次推迟的I/O回调,主要是处理一些系统调用错误,比如网络通信的错误回调。
  3. 在阻塞I/O执行前执行idle, prepare句柄。
  4. 计算轮询I/O的超时时间(注1)。
  5. 根据上一步计算的时间轮询或阻塞I/O循环,文件读写操作等所有I/O相关的回调都会被调用。
  6. 调用检查句柄回调,比如setImmediate
  7. 调用关闭句柄回调
由于setTimeout/setInterval 的第二个参数取值范围是:1 ~ 2^31-1,如果超过这个范围则会初始化为 1,那么就会有:
setTimeout(fn, 0) === setTimeout(fn, 1)
从上面的event loop分析可以看到 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:
  1. timer 前的时间大于1ms,setTimeout的时间就满足了,久先执行setTimeout的回调。
  2. timer 前的时间小于1ms,setTimeout的时间就不满足了,久先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 到 timer 阶段时再去执行setTimeout。
因此 setTimeout 0ms 和 setImmediate 的谁先谁后真的不好说(不过一般来说还是setTimeout更快)。
另外看网上的这个例子:
const fs = require('fs')
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout')
  }, 0)

  setImmediate(() => {
    console.log('setImmediate')
  })
})
因为实在I/O的回调里面,那么接下来的阶段就是check handle call,所以一定是先执行 setImmediate ,再去执行 setTimeout。
const promise = Promise.resolve()
promise.then(() => {
  console.log('promise')
});
process.nextTick(() => {
  console.log('nextTick')
});
虽然promise与process.nextTick都是注册到microTask中,但 process.nextTick 总是早于 promise.then 执行,于是输出结果就是:
nextTick
promise
那么再回到最开始的问题
const promise = Promise.resolve()
setImmediate(() => {
  console.log('setImmediate');
});
setTimeout(() => {
  console.log('setTimeout');
});
promise.then(() => {
  console.log('promise')
});
process.nextTick(() => {
  console.log('nextTick')
});
这四个方法谁先谁后呢?通过上面的分析可以得到结论:
  1. nextTick
  2. promise
  3. setTimeout
  4. setImmediate
注1:
  • 如果使用UV_RUN_NOWAIT标志运行,超时时间为0
  • 如果 uv_stop已经调用了,要停止轮询,超时时间为0
  • 没有活动的句柄或请求,超时时间为0
  • 有任何idle句柄处于活动状态,超时时间为0
  • 如果有任何要关闭的句柄,超时时间为0
  • 如果以上情况均不匹配,如果有timer,那么用距离最近的timer的时间,否则为无穷大