likes
comments
collection
share

nodeJS中的事件循环

作者站长头像
站长
· 阅读数 20

事件循环是nodeJS的特点之一,也是非阻塞异步I/O的基础,上一篇学习中没有升入,因此这篇文章来深入学习一下。

参考文章:

深入理解NodeJS事件循环机制

事件循环规范

Node.js理论实践之《异步非阻塞IO与事件循环》

主要学习node事件循环的两个问题:

1 事件循环详解

2 事件循环中的本轮循环与下轮循环

1 什么是node事件循环

nodejs是单线程的,但是同时也有非阻塞异步IO的特性,那么是怎么在单线程下实现的呢?

node的单线程指的是进程中有一个主线程在执行代码,但是同时也会维护一个事件队列,在遇到网络请求,异步操作( 后面详解 的时候。此操作会先放到事件队列中排队(不会马上执行),同时主线程的代码会继续执行,当主线程代码执行完成之后,通过事件循环机制,检查队列中是否有需要处理的事件,然后从队列头中取出事件,并分配线程池处理事件,全部执行完毕之后,事件队列通知主线程,执行回调,把线程归还给线程池。

具体如下图:

nodeJS中的事件循环

2 node事件循环基本流程

  1. 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack);
  2. 主线程之外,还维护一个事件队列(Event queue),当用户的网络请求或者其它的异步操作到来时,会先进入到事件队列中排队,并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕;
  3. 主线程代码执行完毕完成后,然后通过事件循环机制(Event Loop),检查队列中是否有要处理的事件,从队头取出第一个事件,从线程池分配一个线程来处理这个事件,然后是第二个,第三个,直到队列中所有事件都执行完了。 当有事件执行完毕后,会通知主线程,主线程执行回调,并将线程归还给线程池。这个过程就叫事件循环(Event Loop);
  4. 不断重复上面的第三步;

3 node事件循环的六个阶段

上面的23 可以分为六个阶段(事件循环的六个阶段)

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

概述:

  • 每个阶段都有一个FIFO(先进先出)执行回调函数的队列,每个阶段都有自己的特殊职能,eventLoop进入该阶段是会执行该阶段队列中存在的所有回调函数
  • 跳转下一阶段的条件:1 执行完成该阶段的所有回调函数(队列耗尽);2 超过最大执行限度(nodejs自己规定的参数)
  • poll阶段中处理新的事件可能会加到内核的队列中,也就是处理轮询事件的时候有加入新的轮询事件,所以破阶段的运行时间有可能比计时器规定的时间长(详细看上一篇文章)链接
  • 当所有阶段顺序执行一次之后,称之为EventLoop完成一个tick 进入下一个tick

阶段描述:

  1. timers(定时器)阶段:执行setTimeout和setInterval调度的回调。
  2. pending callbacks(等待回调)阶段: 用于执行前一轮事件循环中被延迟到这一轮的I/O回调函数。
  3. idle,prepare(闲置,准备)阶段: 只能内部使用(官方文档描述 ,没看懂)。
  4. poll(轮询)阶段:最重要的阶段,执行I/O事件回调,除了关闭回调、计时器调度的回调和 setImmediate() 之外的几乎所有异步操作),都将会在这个阶段被处理。
  5. check(检查)阶段:执行 setImmediate 的回调。
  6. close callbacks(关闭回调)阶段:执行close事件的回调, 如套接字(socket)或句柄(handle)突然关闭;

前面也提到当执行到对应异步操作的时候,linux系统支持异步I/O的操作,而windos采用iocp的方式(本质是多线程池)来提供支持。

这里重点讲一下poll阶段(轮询阶段)

轮询阶段主要有两个功能:

  • 当timers的定时器到期后,执行定时器(setTimeout和SetINterval)中的callback
  • 执行poll队列里面的I/O操作,callback

下面是详解:

如果Event Loop进入了poll阶段,且代码没有设定timer(或者timers队列为空)可能会发生以下情况

  1. 如果 poll queue 不为空,Event Loop 将同步的执行 queue 里的 callback,直至 queue 为空,或者执行的 callback 到达系统上限。
  2. 如果 poll queue 为空,可能发生以下情况:
  • 如果代码使用 setImmediate() 设定了 callback,Event Loop 将结束 poll 阶段进入 check 阶段,并执行 check 阶段的 queue。
  • 如果代码没有使用 setImmediate(),Event Loop 将阻塞在该阶段等待 callbacks 加入 poll queue,如果有 callback 进来则立即执行。 一旦 poll queue 为空,Event Loop 将检查 timers,如果有 timer 的时间到期,Event Loop 将回到 timers 阶段,然后执行 timer queue。

仔细对比上面的图,还是比较好理解的。

4 本次循环与次轮循环

上面提到的是单轮异步任务,那么在nodejs中提供两个异步任务同时开启了,先执行谁呢?谁先快就执行谁吗,这显然是不合理的。这就引出了nodejs中的本轮循环与次轮循环

nodejs中的异步任务分为两种

  • 追加在本轮循环中的异步任务:process.nextTick 和 Promise (微任务)的回调函数
  • 追加在次轮循环中的异步任务:setTimeout、setInterval、setImmediate 的回调函数

记住这两个分类,下面也就很好理解了

process.nextTick

1)process.nextTick 不要因为有 next 就被当作次轮循环

2)Node 执行完所有同步任务,接下来就会执行 process.nextTick 的任务队列。

3)开发过程中如果想让异步任务尽可能快地执行,可以使用 process.nextTick 来完成。

promise 微任务(microtack)

根据语言规格,Promise 对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。

微任务队列追加在 process.nextTick 队列的后面,也属于本轮循环。

根据语言规格,Promise 对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。

也就是说promise的回调函数,会在process.nextTick()后面执行

process.nextTick(() => console.log(1))
Promise.resolve().then(() => console.log(2))
process.nextTick(() => console.log(3))
Promise.resolve().then(() => console.log(4))
//输出结果 1,3,2,4

只有前一个队列全部清空以后,才会执行下一个队列。两个队列的概念 nextTickQueue 和微队列 microTaskQueue,也就是说开启异步任务也分为几种,像 promise 对象这种,开启之后直接进入微队列中,微队列内的就是那个任务

5.事件循环中的 setTimeOut 与 setImmediate

由于 setTimeout 在 timers 阶段执行,而 setImmediate 在 check 阶段执行。

所以,setTimeout 会早于 setImmediate 完成。

setTimeout(() => console.log(1))
setImmediate(() => console.log(2))

上面代码应该先输出 1,再输出 2,但是实际执行的时候,结果却是不确定,有时还会先输出 2,再输出 1。

这是因为 setTimeout 的第二个参数默认为 0。但是实际上,Node 做不到 0 毫秒,最少也需要 1 毫秒,根据官方文档,第二个参数的取值范围在 1 毫秒到 2147483647 毫秒之间。也就是说,setTimeout(f, 0)等同于 setTimeout(f, 1)。

实际执行的时候,进入事件循环以后,有可能到了 1 毫秒,也可能还没到 1 毫秒,取决于系统当时的状况。如果没到 1 毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行 setImmediate 的回调函数

6 总结

整体的执行顺序: 同步任务 -本轮循环- 次轮循环