Nodejs事件轮询详解

什么是事件轮询

大家都知道, JavaScript是单线程的, 那么nodejs是如何做到非阻塞呢,在nodejs内部使用了第三方库libuv,nodejs会把IO,文件读取等异步操作交由他处理,而nodejs主线程可以继续去处理其他的事情。libuv会开启不同的线程去处理这些延时操作,处理完后,会把异步操作的回调函数放到nodejs的轮询队列中,nodejs会在适当的时候处理轮询队列中的回调函数,从而实现非阻塞。所以,实际上nodejs在处理这些阻塞操作时,并不是单线程的。

事件轮询详解

下图是nodejs官网的事件轮询流程图

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

从图中可以看出, 事件轮询机制分为六个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

事件轮询阶段详解

timers阶段

概述: timers阶段用来处理setTimeout() 和 setInterval() 的回调函数。
详解 :我们都使用过setTimeout(callback, delay), setInterval(callback, delay), 第一个参数是回调函数, 第二个参数是延迟时间(ms),即多久之后执行。但是在绝大多数情况下,它并不会绝对守时。因为操作系统调度或其它回调的运行可能会延迟它们。比如说, 当轮询进入到poll阶段的时候并且poll阶段的回调队列不为空, 如果timers此时已经有一个setTimeout达到了预设的延时时间, 系统也需要先处理完poll阶段的回调队列,才能去处理timers的回调队列。

pending callbacks阶段

概述: 这个阶段用来处理系统操作的回调函数
详解 :此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,这些错误的回调将被放到此阶段的回调函数。

idle prepare阶段

概述: 此阶段是仅供nodejs内部操作调用,我们不必讨论

poll阶段

概述: 这个阶段主要用来处理如IO操作,网络请求等异步操作
详解 :这各阶段会有不同的情况
1.当poll阶段的回调函数队列不为空的时候,则处理队列中的回调函数,直到队列为空或者达到系统处理的上限的时候,就跳过此阶段,处理下一阶段。
2.当进入poll阶段的时候,如果此阶段的回调队列为空,系统会在此阶段等待新的回调函数入队,再进行处理。如果一直等不到新的回调函数呢?咋办?阻塞在这里?一直等?不会的,在这个阶段会同时进行检测timers阶段是否已经有回调函数超时,如果有,则马上跳过poll阶段,进入下一个阶段。
那么在poll阶段是如何利用libuv库来处理io及文件读取等操作的呢?看下图

libuv处理异步操作.png

从图中可以看得出,libuv自身有一个EventQueue的队列,这个队列里面都是一些如文件读取,网络请求的操作,libuv依次去处理这个队列中的操作,libuv每当从EventQueue中拿到一个处理事件, 就会分配一个线程给它,让这个线程去处理这个事件,当这个线程处理完毕这个事件的时候,就会将结果返回到EventQueue队列, 当再次获取到该事件的时候, 发现已经不是IO操作了,就会把这个事件的回调函数放到poll阶段的队列中,再交由poll去处理回调函数。所以EventQueue在每次出队的时候都会进行判断该操作是否是IO操作,不是的话就直接返回给poll阶段的队列了。需要注意的是,libuv默认只开启了4个线程,你可以通过设置环境变量来修改线程数量。

check阶段

概述: 这个阶段用来处理setImmediate的回调函数
详解 :当poll阶段的回调队列为空的时候(或者达到系统执行的上限),就会进入到check阶段来处理setImmediate的回调函数。

close callbacks阶段

概述: 这个阶段用来处理如socket的close事件
详解 :顾名思义, 关闭回调函数, 如socket.on("close", () => {...})

process.nextTick及Promise

上面的讨论一直未提及process.nextTick和Promise的执行,原因是这两个函数比较特殊,它们不由libuv去管理,而且它们的优先级要高于事件轮询的每一个阶段。它们会在事件轮询的每一个阶段之间执行,注意是事件轮询的每一个阶段之间。process.nextTick会放在nextTickQueue, Promise会放在microTaskQueue,在每次事件轮询进入到下一个阶段的时候, 都会检查这两个队列是否为空,不为空则马上处理它们的回调。
注意因为process.nextTick会在事件轮询每个阶段之间执行, 如果递归调用nextTick, 就会导致轮询阻塞,所以尽量避免使用process.nextTick, 可以使用setImmediate代替。

有趣的事情

从process.nextTick和setImmediate的名字上来看,setImmediate应当是要先于process.nextTick执行的。但事实恰好相反,nodejs官网也给出了解释,这是nodejs历史原因,很难再去修改他们的名字!!

最后贴出整个事件轮询的完整流程图

屏幕快照 2019-09-08 下午2.43.06.png

你可能感兴趣的:(Nodejs事件轮询详解)