Node事件循环

Node.js中的事件循环

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

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

Node.js事件循环机制

setImmediatesetTimeout对比

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

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

//run it
node index.js

上述代码的执行结果并不固定,在介绍setImmediatesetTimeout的区别之前,我们先来讨论下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│
 └───────────────────────┘

nextTicksetImmediate

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

  • 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回调执行完毕后打印出来。