Node 事件循环

Node.js 中的事件循环

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

  • Node 端,microtask 在事件循环的各个阶段之间执行
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

Node.js 事件循环机制

setImmediate 与 setTimeout 对比

上文中我们介绍过 setImmediate 与 setTimeout 都属于 MacroTask,每轮事件循环中都会执行 MacroTask 中的队列首部回调;不过我们也经常可以看到描述说 setImmediate 会优于 setTimeout 执行,

//index.js
setTimeout(function(){
  console.log("SETTIMEOUT");
});
setImmediate(function(){
  console.log("SETIMMEDIATE");
});

//run it
node index.js

上述代码的执行结果并不固定,在介绍 setImmediate 与 setTimeout 的区别之前,我们先来讨论下 Node.js 中的事件循环机制;其基本流程如下图所示:

 ┌───────────────────────┐
┌─>timers 
│└──────────┬────────────┘
│┌──────────┴────────────┐
││ IO callbacks 
│└──────────┬────────────┘
│┌──────────┴────────────┐
││ idle, prepare 
│└──────────┬────────────┘┌───────────────┐
│┌──────────┴────────────┐│ incoming: 
││ poll<─────┤connections, 
│└──────────┬────────────┘│ data, etc.
│┌──────────┴────────────┐└───────────────┘
││check
│└──────────┬────────────┘
│┌──────────┴────────────┐
└──┤close callbacks
 └───────────────────────┘
//index.js
var fs = require('fs');
fs.readFile("my-file-path.txt", function() {
  setTimeout(function(){
  console.log("SETTIMEOUT");
  });
  setImmediate(function(){
  console.log("SETIMMEDIATE");
  });
});

//run it
node index.js

//output (always)
SETIMMEDIATE
SETTIMEOUT
var Suite = require('benchmark').Suite
var fs = require('fs')


var suite = new Suite


suite.add('deffered.resolve()', function(deferred) {
  deferred.resolve()
}, {defer: true})


suite.add('setImmediate()', function(deferred) {
  setImmediate(function() {
  deferred.resolve()
  })
}, {defer: true})


suite.add('setTimeout(,0)', function(deferred) {
  setTimeout(function() {
  deferred.resolve()
  },0)
}, {defer: true})


suite
.on('cycle', function(event) {
  console.log(String(event.target));
})
.on('complete', function() {
  console.log('Fastest is ' + this.filter('fastest').pluck('name'));
})
.run({async: true})


// 输出
deffered.resolve() x 993 ops/sec ±0.67% (22 runs sampled)
setImmediate() x 914 ops/sec ±2.48% (57 runs sampled)
setTimeout(,0) x 445 ops/sec ±2.79% (82 runs sampled)

浏览器中实现 setImmediate

当我们使用 Webpack 打包应用时,其默认会添加 setImmediate 的垫片

Node.js Event Loop

 ┌───────────────────────┐
┌─>│timers │
│└──────────┬────────────┘
│┌──────────┴────────────┐
││ IO callbacks │
│└──────────┬────────────┘
│┌──────────┴────────────┐
││ idle, prepare │
│└──────────┬────────────┘┌───────────────┐
│┌──────────┴────────────┐│ incoming: │
││ poll│<─────┤connections, │
│└──────────┬────────────┘│ data, etc.│
│┌──────────┴────────────┐└───────────────┘
││check│
│└──────────┬────────────┘
│┌──────────┴────────────┐
└──┤close callbacks│
 └───────────────────────┘

nextTick 与 setImmediate

我们通过比较以下两个用例来了解 setImmediate 与 nextTick 的区别:

  • setImmediate
setImmediate(function A() {
  setImmediate(function B() {
  log(1);
  setImmediate(function D() { log(2); });
  setImmediate(function E() { log(3); });
  });
  setImmediate(function C() {
  log(4);
  setImmediate(function F() { log(5); });
  setImmediate(function G() { log(6); });
  });
});


setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)


// 'TIMEOUT FIRED' 1 4 2 3 5 6
// OR
// 1 'TIMEOUT FIRED' 4 2 3 5 6
  • nextTick
process.nextTick(function A() {
  process.nextTick(function B() {
    log(1);
    process.nextTick(function D() {
      log(2);
    });
    process.nextTick(function E() {
      log(3);
    });
  });
  process.nextTick(function C() {
    log(4);
    process.nextTick(function F() {
      log(5);
    });
    process.nextTick(function G() {
      log(6);
    });
  });
});

setTimeout(function timeout() {
  console.log("TIMEOUT FIRED");
}, 0);

// 1 4 2 3 5 6 'TIMEOUT FIRED'

如上文所述,通过 setImmediate 设置的回调会以 MacroTask 加入到 Event Loop 中,每个循环中会提取出某个回调执行;setImmediate 能够避免 Event Loop 被阻塞,从而允许其他完成的 IO 操作或者定时器回调顺利执行。而通过 nextTick 加入的回调会在当前代码执行完毕(即函数调用栈执行完毕)后立刻执行,即会在返回 Event Loop 之前立刻执行。譬如上面的例子中,setTimeout 的回调会在 Event Loop 中调用,因此 TIMEOUT FIRED 的输出会在所有的 nextTick 回调执行完毕后打印出来。