1.nodejs 为什么要存在一个event loop的事件处理机制
nodejs 具有事件驱动和非阻塞但线程的特点,使相关应用变得比较轻量和高效。当应用程序需要相关I/O操作时,线程并不会阻塞,而是把I/O操作移交给底层类库(如:libuv)。此时nodejs线程会去处理其他的任务,当底层库处理完相关的I/O操作后,会将主动权再次交还给nodejs线程。因此event loop的作用就是起到调度线程的作用,如当底层类库处理I/O操作后调度nodejs单线程处理后续的工作。也就是说当nodejs 程序启动的时候,它会开启一个event loop以实现异步的api调度、schedule timers 、回调process.nextTick()。
从上也可以看出nodejs 虽说是单线程,但是在底层类库处理异步操作的时候仍然是多线程。
2.引出问题
在node环境中我们运行如下代码,会出现怎么样的执行结果?
let fs = require('fs');
setTimeout(function(){
Promise.resolve().then(()=>{
console.log('then2');
})
},0);
Promise.resolve().then(()=>{
console.log('then1');
});
fs.readFile('./gitigore',function(){
process.nextTick(function(){
console.log('nextTick')
})
setImmediate(()=>{
console.log('setImmediate')
});
});
复制代码
在node环境的执行结果是
then1
then2
nextTick
setImmediate
复制代码
3.开始事件循环之前,nodejs初始化
产出这样的结果,来源于Node.js对事件的循环操作顺序。在Node.js的官方文档中,对初始化event loop有这样的描述 The Node.js Event Loop, Timers, and process.nextTick()
-当Node.js启动的时候,他会初始化Event Loop,处理提供的输入脚本,这可能会使异步API调用,调用timers,或调用process.nextTick,然后开始处理事件循环,下面是一个经典的事件循环操作顺序 ┌───────────────────────────────────┐
┌─>│timers(计时器)执行 │
│ |setTimeout以及setInterval的回调 │
│ └──────────┬────────────────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ 处理网络,流,TCP的错误 │
│ │ callback │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ │ node内部使用 │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │poll(轮询) │<─────┤ connections, │
│ │ 执行poll中的i/o队列检查 │ │data, etc. │
│ │定时器是否到时 │ └───────────────┘
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ check │
│ │ 存放setImmediate回调 │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
│ 关闭的回调例如 │
│ socket.on('close') │
└───────────────────────┘
复制代码
其中,每个盒子都是 Event Loop的一个阶段,当Event Loop进入到某个阶段的时候,就会将该阶段队列里的回调拿出来执行,直到队列为空。
几个队列
Timers Queue - 计时器队列
I/O Queue - 输入输出队列
Check Queue - 检查队列
Close Queue - guangbi 队列
复制代码
除了上面循环阶段的任务类型,还有浏览器和nodejs共有的微任务(micro task)和node的 process.nextTick。分别称其对应的队列为MircoTask Queue和NextTick Queue
4. 开始循环之后:
依据上述6个阶段依次执行,每次拿出当前阶段的全部任务执行,清空NextTick队列,清空微任务队列,再执行下一阶段,全部6个阶段完毕后,进入下一轮的循环。
即用一张图表述为
- 结合代码
let fs = require('fs');
setTimeout(function(){
Promise.resolve().then(()=>{
console.log('then2');
})
},0);
Promise.resolve().then(()=>{
console.log('then1');
});
fs.readFile('./gitigore',function(){
process.nextTick(function(){
console.log('nextTick')
})
setImmediate(()=>{
console.log('setImmediate')
});
});
复制代码
回看我们开头展示的代码,这里我们的队列中显然包含有
setTimeout
Promise.resolve().then
fs.readFile
复制代码
这样的三个主要的任务队列 依据循环阶段,我们将代码按照循环阶段的顺序展示和执行
// 清空TimerQueue
setTimeout(...)
// 清空该进程中的微任务
// then1位置的Promise先进入任务队列
Promise.resolve().then(()=>{
console.log('then1'); // then1
});
Promise.resolve().then(()=>{
console.log('then2'); // then2
})
// 接着进入IO队列
fs.readFile(...)
// 优先清空IO队列的NextTick Queue
process.nextTick(function(){
console.log('nextTick') // nextTick
})
// 清空micro queue
setImmediate(()=>{
console.log('setImmediate')//setImmediate
});
复制代码