浅谈Node.js的事件环(event loop)

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个阶段完毕后,进入下一轮的循环。

即用一张图表述为

  1. 结合代码
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
});

复制代码

猜你喜欢

转载自juejin.im/post/5b5dcb3e5188257bcb59083f