ALL THE TIME,咱们写的的大部分javascript
代码都是在浏览器环境下编译运行的,所以可能咱们对浏览器的事件循环机制了解比Node.JS
的事件循环更深刻一些,可是最近写开始深刻NodeJS学习的时候,发现NodeJS的事件循环机制和浏览器端有很大的区别,特此记录来深刻的学习了下,以帮助本身及小伙伴们忘记后查阅及理解。javascript
参考资料:java
首先咱们须要了解一下最基础的一些东西,好比这个事件循环,事件循环是指Node.js执行非阻塞I/O操做,尽管==JavaScript是单线程的==,但因为大多数==内核都是多线程==的,Node.js
会尽量将操做装载到系统内核。所以它们能够处理在后台执行的多个操做。当其中一个操做完成时,内核会告诉Node.js
,以便Node.js
能够将相应的回调添加到轮询队列中以最终执行。node
当Node.js启动时会初始化event loop
, 每个event loop
都会包含按以下顺序六个循环阶段:segmentfault
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
复制代码
timers
阶段: 这个阶段执行 setTimeout(callback)
和 setInterval(callback)
预约的 callback;I/O callbacks
阶段: 此阶段执行某些系统操做的回调,例如TCP错误的类型。 例如,若是TCP套接字在尝试链接时收到 ECONNREFUSED,则某些* nix系统但愿等待报告错误。 这将操做将等待在==I/O回调阶段==执行;idle, prepare
阶段: 仅node内部使用;poll
阶段: 获取新的I/O事件, 例如操做读取文件等等,适当的条件下node将阻塞在这里;check
阶段: 执行 setImmediate()
设定的callbacks;close callbacks
阶段: 好比 socket.on(‘close’, callback)
的callback会在这个阶段执行;应用层
、
V8引擎层
、
Node API层
和
LIBUV层
。
- 应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,好比 http,fs
- V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
- NodeAPI层: 为上层模块提供系统调用,通常是由 C 语言来实现,和操做系统进行交互 。
- LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操做等,是 Node.js 实现异步的核心 。
timers
阶段 一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间事后,timers会尽量早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。promise
注意:技术上来讲,poll 阶段控制 timers 何时执行。浏览器
注意:这个下限时间有个范围:[1, 2147483647],若是设定的时间不在这个范围,将被设置为1。bash
I/O callbacks
阶段 这个阶段执行一些系统操做的回调。好比TCP错误,如一个TCP socket在想要链接时收到ECONNREFUSED, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行. 名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.多线程
poll
阶段 poll 阶段有两个主要功能:(1)执行下限时间已经达到的timers的回调,(2)而后处理 poll 队列里的事件。 当event loop进入 poll 阶段,而且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:异步
若是 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;socket
若是 poll 队列为空,则发生如下两件事之一:
可是,当event loop进入 poll 阶段,而且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态): event loop将检查timers,若是有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。
check
阶段 这个阶段容许在 poll 阶段结束后当即执行回调。若是 poll 阶段空闲,而且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。
setImmediate() 其实是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv
的API 来设定在 poll 阶段结束后当即执行回调。
一般上来说,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。可是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。
close callbacks
阶段 若是一个 socket 或 handle 被忽然关掉(好比 socket.destroy()),close事件将在这个阶段被触发,不然将经过process.nextTick()触发
// 事件循环自己至关于一个死循环,当代码开始执行的时候,事件循环就已经启动了
// 而后顺序调用不一样阶段的方法
while(true){
// timer阶段
timer()
// I/O callbacks阶段
IO()
// idle阶段
IDLE()
// poll阶段
poll()
// check阶段
check()
// close阶段
close()
}
// 在一次循环中,当事件循环进入到某一阶段,加入进入到check阶段,忽然timer阶段的事件就绪,也会等到当前此次循环结束,再去执行对应的timer阶段的回调函数
// 下面看这里例子
const fs = require('fs')
// timers阶段
const startTime = Date.now();
setTimeout(() => {
const endTime = Date.now()
console.log(`timers: ${endTime - startTime}`)
}, 1000)
// poll阶段(等待新的事件出现)
const readFileStart = Date.now();
fs.readFile('./Demo.txt', (err, data) => {
if (err) throw err
let endTime = Date.now()
// 获取文件读取的时间
console.log(`read time: ${endTime - readFileStart}`)
// 经过while循环将fs回调强制阻塞5000s
while(endTime - readFileStart < 5000){
endTime = Date.now()
}
})
// check阶段
setImmediate(() => {
console.log('check阶段')
})
/*控制台打印 check阶段 read time: 9 timers: 5008 经过上述结果进行分析, 1.代码执行到定时器setTimeOut,目前timers阶段对应的事件列表为空,在1000s后才会放入事件 2.事件循环进入到poll阶段,开始不断的轮询监听事件 3.fs模块异步执行,根据文件大小,可能执行时间长短不一样,这里我使用的小文件,事件大概在9s左右 4.setImmediate执行,poll阶段暂时未监测到事件,发现有setImmediate函数,跳转到check阶段执行check阶段事件(打印check阶段),第一次时间循环结束,开始下一轮事件循环 5.由于时间仍未到定时器截止时间,因此事件循环有一次进入到poll阶段,进行轮询 6.读取文件完毕,fs产生了一个事件进入到poll阶段的事件队列,此时事件队列准备执行callback,因此会打印(read time: 9),人工阻塞了5s,虽然此时timer定时器事件已经被添加,可是由于这一阶段的事件循环为完成,因此不会被执行,(若是这里是死循环,那么定时器代码永远没法执行) 7.fs回调阻塞5s后,当前事件循环结束,进入到下一轮事件循环,发现timer事件队列有事件,因此开始执行 打印timers: 5008 ps: 1.将定时器延迟时间改成5ms的时候,小于文件读取时间,那么就会先监听到timers阶段有事件进入,从而进入到timers阶段执行,执行完毕继续进行事件循环 check阶段 timers: 6 read time: 5008 2.将定时器事件设置为0ms,会在进入到poll阶段的时候发现timers阶段已经有callback,那么会直接执行,而后执行完毕在下一阶段循环,执行check阶段,poll队列的回调函数 timers: 2 check阶段 read time: 7 */
复制代码
咱们来看一个简单的EventLoop
的例子:
const fs = require('fs');
let counts = 0;
// 定义一个 wait 方法
function wait (mstime) {
let date = Date.now();
while (Date.now() - date < mstime) {
// do nothing
}
}
// 读取本地文件 操做IO
function asyncOperation (callback) {
fs.readFile(__dirname + '/' + __filename, callback);
}
const lastTime = Date.now();
// setTimeout
setTimeout(() => {
console.log('timers', Date.now() - lastTime + 'ms');
}, 0);
// process.nextTick
process.nextTick(() => {
// 进入event loop
// timers阶段以前执行
wait(20);
asyncOperation(() => {
console.log('poll');
});
});
/** * timers 21ms * poll */
复制代码
这里呢,为了让这个setTimeout
优先于fs.readFile
回调, 执行了process.nextTick
, 表示在进入timers
阶段前, 等待20ms
后执行文件读取.
nextTick
与 setImmediate
process.nextTick
不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感受.
setImmediate
的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.
因为nextTick具备插队的机制,nextTick的递归会让事件循环机制没法进入下一个阶段. 致使I/O处理完成或者定时任务超时后仍然没法执行, 致使了其它事件处理程序处于饥饿状态. 为了防止递归产生的问题, Node.js 提供了一个 process.maxTickDepth (默认 1000)。
const fs = require('fs');
let counts = 0;
function wait (mstime) {
let date = Date.now();
while (Date.now() - date < mstime) {
// do nothing
}
}
function nextTick () {
process.nextTick(() => {
wait(20);
console.log('nextTick');
nextTick();
});
}
const lastTime = Date.now();
setTimeout(() => {
console.log('timers', Date.now() - lastTime + 'ms');
}, 0);
nextTick();
复制代码
此时永远没法跳到timer
阶段去执行setTimeout里面的回调方法
, 由于在进入timers
阶段前有不断的nextTick
插入执行. 除非执行了1000次到了执行上限,因此上面这个案例会不断地打印出nextTick
字符串
setImmediate
若是在一个I/O周期
内进行调度,setImmediate() 将始终在任何定时器(setTimeout、setInterval)以前执行.
setTimeout
与 setImmediate
无 I/O 处理状况下:
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
复制代码
执行结果:
C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate
C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate
C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
复制代码
从结果,咱们能够发现,这里打印输出出来的结果,并无什么固定的前后顺序,偏向于随机,为何会发生这样的状况呢?
答:首先进入的是timers
阶段,若是咱们的机器性能通常,那么进入timers
阶段,1ms
已通过去了 ==(setTimeout(fn, 0)等价于setTimeout(fn, 1))==,那么setTimeout
的回调会首先执行。
若是没有到1ms
,那么在timers
阶段的时候,下限时间没到,setTimeout
回调不执行,事件循环来到了poll
阶段,这个时候队列为空,因而往下继续,先执行了setImmediate()的回调函数,以后在下一个事件循环再执行setTimemout
的回调函数。
问题总结:而咱们在==执行启动代码==的时候,进入timers
的时间延迟实际上是==随机的==,并非肯定的,因此会出现两个函数执行顺序随机的状况。
那咱们再来看一段代码:
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
复制代码
打印结果以下:
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
# ... 省略 n 屡次使用 node test.js 命令 ,结果都输出 immediate timeout
复制代码
这里,为啥和上面的随机timer
不一致呢,咱们来分析下缘由:
缘由以下:fs.readFile
的回调是在poll
阶段执行的,当其回调执行完毕以后,poll
队列为空,而setTimeout
入了timers
的队列,此时有代码 setImmediate()
,因而事件循环先进入check
阶段执行回调,以后在下一个事件循环再在timers
阶段中执行回调。
固然,下面的小案例同理:
setTimeout(() => {
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
}, 0);
复制代码
以上的代码在timers
阶段执行外部的setTimeout
回调后,内层的setTimeout
和setImmediate
入队,以后事件循环继续日后面的阶段走,走到poll阶段
的时候发现队列为空
,此时有代码有setImmedate()
,因此直接进入check阶段
执行响应回调(==注意这里没有去检测timers队列中是否有成员
到达下限事件,由于setImmediate()优先
==)。以后在第二个事件循环的timers
阶段中再去执行相应的回调。
综上所演示,咱们能够总结以下:
setImmediate的回调永远先执行
**。nextTick
与 Promise
概念:对于这两个,咱们能够把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。 那么他们是在何时执行呢? 无论在什么地方调用,他们都会在其所处的事件循环最后,事件循环进入下一个循环的阶段前执行。
setTimeout(() => {
console.log('timeout0');
new Promise((resolve, reject) => { resolve('resolved') }).then(res => console.log(res));
new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('timeout resolved')
})
}).then(res => console.log(res));
process.nextTick(() => {
console.log('nextTick1');
process.nextTick(() => {
console.log('nextTick2');
});
});
process.nextTick(() => {
console.log('nextTick3');
});
console.log('sync');
setTimeout(() => {
console.log('timeout2');
}, 0);
}, 0);
复制代码
控制台打印以下:
C:\Users\92809\Desktop\node_test>node test.js
timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout2
timeout resolved
复制代码
最总结:timers
阶段执行外层setTimeout
的回调,遇到同步代码先执行,也就有timeout0
、sync
的输出。遇到process.nextTick
及Promise
后入微任务队列,依次nextTick1
、nextTick3
、nextTick2
、resolved
入队后出队输出。以后,在下一个事件循环的timers
阶段,执行setTimeout
回调输出timeout2
以及微任务Promise
里面的setTimeout
,输出timeout resolved
。(这里要说明的是 微任务nextTick
优先级要比Promise
要高)
代码片断1:
setImmediate(function(){
console.log("setImmediate");
setImmediate(function(){
console.log("嵌套setImmediate");
});
process.nextTick(function(){
console.log("nextTick");
})
});
/* C:\Users\92809\Desktop\node_test>node test.js setImmediate nextTick 嵌套setImmediate */
复制代码
解析:
事件循环check
阶段执行回调函数输出setImmediate
,以后输出nextTick
。嵌套的setImmediate
在下一个事件循环的check
阶段执行回调输出嵌套的setImmediate
。
代码片断2:
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end')
复制代码
打印结果为:
C:\Users\92809\Desktop\node_test>node test.js
script start
async1 start
async2
promise1
promise2
script end
nextTick
promise3
async1 end
setTimeout0
setTimeout3
setImmediate
复制代码
你们呢,能够先看着代码,默默地在心底走一变代码,而后对比输出的结果,固然最后三位,我我的认为是有点问题的,毕竟在主模块运行,你们的答案,最后三位可能会有误差;