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 阶段会消耗一定时间,所以就会出现两种情况:
- timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数
- 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 的回调函数执行完后:
- 注册 setTimeout 的回调函数到 timer 阶段
- 注册 setImmediate 的回调函数到 check 阶段
- event loop 从 pool 阶段出来继续往下一个阶段执行,恰好是 check 阶段,所以 setImmediate 的回调函数先执行
- 本次 event loop 结束后,进入下一次 event loop,执行 setTimeout 的回调函数
所以,在 I/O Callbacks 中注册的 setTimeout 和 setImmediate,永远都是 setImmediate 先执行。
宏任务和微任务
这两个概念属于对异步任务的分类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。
task主要包含:setTimeout、setInterval、setImmediate、I/O、UI交互事件
microtask主要包含:Promise、process.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待续。。。。。。