在Node.js中,事件循环是实现异步I/O的关键,是必须要了解的知识。
这篇笔记是我读官方文档里关于事件循环的文章得来的。
事件循环的概念
事件循环使单线程的JavaScript实现了异步I/O操作,通过将负载交给系统内核执行。由于大多数的现代操作系统都是多线程的,能在后台执行多任务的操作。当后台操作完成后,内核(kernel)通知Node.js,这样的话回调函数就可以添加到poll队列中,直到执行完成。
事件循环的执行顺序
图片中的每个阶段称为phase (图片来自libuv文档)
每个阶段(phase)都有一个待执行的回调函数FIFO队列,每一个阶段都有所不同。事件循环每进行到一个阶段,就会执行当前阶段特有的操作,然后执行回调函数,直至将回调函数的队列清空或者达到设置的最大限制。
timers
该阶段执行的是由*setTimeout()和setInterval()*设置的回调函数。
从技术上来讲,poll阶段控制着timers阶段什么时候执行。比如下面的代码:
1 | const fs = require('fs'); |
第一个setTimeout设置的时间是900ms,但是用console.timeEnd()的输出用时却是大于900ms的,在我的电脑上大约是902ms左右。这是因为当事件循环进行到poll阶段时,发现队列中没有任务,然后等待了一会儿(小于 900ms),然后读文件操作执行完了,并且将回调函数添加到了poll队列中,然后执行完该回到函数后(大概用了2ms)发现setTimeout()设置的时间到了,然后执行了其回调函数。所以setTimeout()的回调函数的执行等待了大于900ms的时间。
为了防止poll阶段饿死事件循环,libuv设置了poll阶段可以行的回调函数的最大数量限制,该限制在不同的的系统上不相同。
pending callbacks
执行延迟到下一个阶段的I/O回调。
该阶段执行一些系统操作的回调,比如TCP的链接错误。
idle,prepare
内部使用(不太明白内部使用是什么意思)。
poll
- 获取新的I/O事件;
- 执行几乎所有I/O相关的回调函数,除了:close callback,由timers和*setImmediate()*设置的回调。
Node.js可能会阻塞在该阶段。
当事件循环进行到该阶段,并且没有设置timers时,有可能会发生下面的两件事情之一:
- 如果poll队列不为空,事件循环会处理队列里的回调函数,直到处理完成或者达到系统的最大限制;
- 如果poll队列为空,又会发生下面的两件事情之一:
- 如果设置了setImmediate(),则事件循环会结束poll阶段,然后进入到check阶段,去执行setImmediate设置的回调函数;
- 如果没有设置*setImmediate()*,则事件循环会等待回调函数添加到队列,然后立即执行它们。
一旦poll队列为空,事件循环会去检查是否有timers到时间,如果有,就返回timers阶段执行到期的timers
check
执行*setImmediate()*设置的回调。
该阶段主要是用来执行poll阶段后需要立即执行的操作的。如果poll阶段闲置了,并且setImmediate()设置了一些需要执行的脚本,循环就会直接进入check阶段。setImmediate()其实是一个特殊的timer.
close callback
关闭事件的回调函数,比如socket.on('close', ...)
。