从实例代码讲解Node.js Event loop执行机制(1.0.1)

769 阅读4分钟
2018-03-23 星期五 农历 二月初七戊戌年 【狗年】乙卯月 甲寅日
宜:  裁衣、经络、伐木、开柱眼、拆卸、修造、动土、上梁、合脊、合寿木、入殓、除服、成服、移柩、破土
忌:  祭祀、嫁娶、出行、上梁、掘井


本文采用 自问自答的形式 配合代码(执行结果)来讲解 event loop 的执行机制

执行环境 :  node.js-8.10.0  WebStorm 2017.2.3 

example 1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3)
})
setTimeout(function(){
    console.log(4);
})
console.log(5)

 执行:输入结果 显示如下


现在我们来实例分析下 这个代码执行结果为什么是这样的 : 

   Node是基于单线程的(主要主线程是单线程,用来执行同步任务。等遇到异步任务的时候,会调用另外一条异步线程来处理异步任务,如:setTimeout 这些会影响主线程运行的,需要等待一段时间)   

代码执行分析 如下:   主线程开始执行,自上往下,先是开始执行同步任务, 依次执行

console.log(1)
console.log(2) 

然后遇到 异步任务

setTimeout(function(){
    console.log(3)
})
setTimeout(function(){
    console.log(4);
})

这时候 Node 会把 这些异步任务 push 到一个 异步执行栈 stack  里面  

然后继续执行 主线程的 任务

console.log(5) 

等到所有的同步任务任务,这时候 Node  开始执行  异步队列  栈 stack 里面的异步任务 

按照 堆 stack 的特征 先进后出的特征   会依次开始执行 栈里面的异步任务  

等待他们执行完毕以后,会生成一个 宏  任务队列(下面会有详细的介绍),按照执行前后顺序来添加到这个队列中,

队列遵循先进先出的规则,开始依次执行 列队

执行结果如下:

3
4


现在来更深入的了解下 Node.js event loop 机制

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,
   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时, node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时, event loop会转入下一下阶段.

注意上面六个阶段都不包括 process.nextTick() (稍后会在宏任务和微任务)

example 2

setTimeout(() => {
  console.log('setTimeout')
}, 0)

setImmediate(() => {
  console.log('setImmediate')
})

运行结果:

setImmediate
setTimeout

或者:

setTimeout
setImmediate

为什么结果不确定呢?

解释:setTimeout/setInterval 的第二个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:

  1. timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数
  2. timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数

example 3

const fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout')
  }, 0)

  setImmediate(() => {
    console.log('setImmediate')
  })
})

运行结果:

setImmediate
setTimeout

解释:fs.readFile 的回调函数执行完后:

  1. 注册 setTimeout 的回调函数到 timer 阶段
  2. 注册 setImmediate 的回调函数到 check 阶段
  3. event loop 从 pool 阶段出来继续往下一个阶段执行,恰好是 check 阶段,所以 setImmediate 的回调函数先执行
  4. 本次 event loop 结束后,进入下一次 event loop,执行 setTimeout 的回调函数

所以,在 I/O Callbacks 中注册的 setTimeout 和 setImmediate,永远都是 setImmediate 先执行。


宏任务和微任务

这两个概念属于对异步任务的分类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。

task主要包含:setTimeoutsetIntervalsetImmediateI/OUI交互事件

microtask主要包含:Promiseprocess.nextTick

microtask 会优先 task 执行 

一般 Event loop 会先清空 microtask 队列里面的任务,然后才回去 执行 task 里面的 异步任务

example 4

console.log(1);
console.log(2);
setImmediate(function(){
    console.log(4);
})
setTimeout(function(){
    console.log(3)
})
process.nextTick(function(){
    console.log('process.nextTick')
})
Promise.resolve().then(function () {
    console.log('Promise')
})

执行结果如下: 

1 
2
process.nextTick 
Promise 
3 
4

先依次执行同步任务 console.log 输出

1
2

然后开始执行异步任务 stack 里面的任务 先微任务在宏任务

输出

process.nextTick 
Promise 

最后等待微任务执行完毕以后,在执行最后的宏任务

3 
4


待续。。。。。。