NodeJS中的异步方法

因为都是基于 V8 引擎,浏览器中包含的异步方式在 NodeJS 中也是一样的。另外 NodeJS 中还有一些其他常见异步形式。

  • 文件 I/O:异步加载本地文件。
  • setImmediate():与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行。
  • process.nextTick():在某些同步任务完成后立马执行。
  • server.close、socket.on(‘close’,…)等:关闭回调。

想象一下,如果上面的形式和 setTimeout、promise 等同时存在,如何分析出代码的执行顺序呢?只要我们理解了 NodeJS 的事件循环机制,也就清楚了。

事件循环模型

NodeJS 中 V8 引擎将 JS 代码解析后调用 Node API,然后 Node API 将任务交给 Libuv 去分配,最后再将执行结果返回给 V8 引擎。在 Libux 中实现了一套事件循环流程来管理这些任务的执行,所以 NodeJS 的事件循环主要是在 Libuv 中完成的
image.png

事件循环各个阶段

  • timers 阶段:执行所有 setTimeout() 和 setInterval() 的回调。
  • pending callbacks 阶段:某些系统操作的回调,如 TCP 链接错误。除了 timers、close、setImmediate 的其他大部分回调在此阶段执行。
  • poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers 计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待。
  • check 阶段:setImmediate 回调函数执行。
  • close callbacks 阶段:关闭回调执行,如 socket.on(‘close’, …)。

image.png

  1. const fs = require('fs');
  2. fs.readFile(__filename, (data) => {
  3. // poll(I/O 回调) 阶段
  4. console.log('readFile')
  5. Promise.resolve().then(() => {
  6. console.error('promise1')
  7. })
  8. Promise.resolve().then(() => {
  9. console.error('promise2')
  10. })
  11. });
  12. setTimeout(() => {
  13. // timers 阶段
  14. console.log('timeout');
  15. Promise.resolve().then(() => {
  16. console.error('promise3')
  17. })
  18. Promise.resolve().then(() => {
  19. console.error('promise4')
  20. })
  21. }, 0);
  22. // 下面代码只是为了同步阻塞1秒钟,确保上面的异步任务已经准备好了
  23. var startTime = new Date().getTime();
  24. var endTime = startTime;
  25. while(endTime - startTime < 1000) {
  26. endTime = new Date().getTime();
  27. }
  28. // 最终输出 timeout promise3 promise4 readFile promise1 promise2
  1. setTimeout(() => {
  2. console.log('timeout1')
  3. Promise.resolve().then(function() {
  4. console.log('promise1')
  5. })
  6. }, 0);
  7. setTimeout(() => {
  8. console.log('timeout2')
  9. Promise.resolve().then(function() {
  10. console.log('promise2')
  11. })
  12. }, 0);
  • 浏览器中运行每次宏任务完成后都会优先处理微任务,输出“timeout1”、“promise1”、“timeout2”、“promise2”。
  • NodeJS 中运行因为输出 timeout1 时,当前正处于 timers 阶段,所以会先将所有 timer 回调执行完之后再执行微任务队列,即输出“timeout1”、“timeout2”、“promise1”、“promise2”。

上面的差异可以用浏览器和 NodeJS 10 对比验证。是不是感觉有点反程序员?因此 NodeJS 在版本 11 之后,就修改了此处逻辑使其与浏览器尽量一致,也就是每个 timer 执行后都先去检查一下微任务队列,所以 NodeJS 11 之后的输出已经和浏览器一致了。

nextTick、setImmediate 和 setTimeout

setImmediate: check阶段执行的
process.nextTick(): 可以想象是把 nextTick 的任务放到了当前循环的后面,与 promise.then() 类似,但比 promise.then() 更前面。意思就是在当前同步代码执行完成后,不管其他异步任务,先尽快执行 nextTick
setTimeout(): time阶段执行的

  1. setTimeout(() => {
  2. console.log('timeout');
  3. }, 0);
  4. Promise.resolve().then(() => {
  5. console.error('promise')
  6. })
  7. process.nextTick(() => {
  8. console.error('nextTick')
  9. })
  10. // 输出:nextTick、promise、timeout
  1. setTimeout(() => {
  2. console.log('timeout');
  3. }, 0);
  4. setImmediate(() => {
  5. console.log('setImmediate');
  6. });
  7. // 输出:timeout、 setImmediate
  1. const fs = require('fs');
  2. fs.readFile(__filename, (data) => {
  3. console.log('readFile');
  4. setTimeout(() => {
  5. console.log('timeout');
  6. }, 0);
  7. setImmediate(() => {
  8. console.log('setImmediate');
  9. });
  10. });
  11. // 输出:readFile、setImmediate、timeout
  • 第一轮循环没有需要执行的异步任务队列;
  • 第二轮循环 timers 等阶段都没有任务,只有 poll 阶段有 I/O 回调任务,即输出“readFile”;
  • 参考前面事件阶段的说明,接下来,poll 阶段会检测如果有 setImmediate 的任务队列则进入 check 阶段,否则再进行判断,如果有定时器任务回调,则回到 timers 阶段,所以应该进入 check 阶段执行 setImmediate,输出“setImmediate”;
  • 然后进入最后的 close callbacks 阶段,本次循环结束;
  • 最后进行第三轮循环,进入 timers 阶段,输出“timeout”。