在 node.js 中,事件循环是 libuv 提供的能力。启动 node.js , 会开启一个进程。
事件循环会检测有无任务,如果没有任务时,进程就会退出。比如,执行 node main.js ,而 main.js 仅有一行 console ,执行完就自动退出进程了。 而当 node.js 提供 TCP 服务监听某一端口时,就不会退出进程。
执行完传入的 js, 就进入下图事件循环的某个阶段。

- timer: 执行 setTimeout、setInterval 回调;
- I/O callbacks: 执行系统调用错误,如网络通信错误回调等;
- idle、prepare: node 内部使用,可以忽略;
- poll: 获取新的 I/O 事件,适当条件(下面将,主要是 handle 或 req 活跃时)下 node 阻塞在这里,等待唤醒(停在这里停多久,这里有个算法,比如又一个定时器一秒钟过期,那就最多停一秒,保证超时回调,如果有其他事件唤醒了就提前唤醒);
- check: 执行 setImmediate 回调;
- close callbacks: 执行关闭服务器这些资源时的回调;
对于上述阶段的理解:
- 上述每个阶段都维护了一个宏任务队列,处理队列里的宏任务,就是在回调 js 层函数,回调完就会执行微任务队列的所有任务。
- 不同的宏任务会往不同阶段的宏任务队列里塞,比如 setTimeout 就是 timer 队列。
接下来解释,什么条件会阻塞在 poll io 阶段,而不退出进程:

handle 就是监听了端口,此时 uv_has_active_handles 为 true;
request 就是读取文件。此时 uv__has_active_reqs 为 true。
handles 和 requests 是 libuv 核心的概念。libuv 的文档有如下解释:

整个 libuv 的实现都是基于 Handle 和 Request。
handles 能够操作那些长时间活跃的对象,例如:
活跃的 handle 在每一次 event loop 期间会被调用一次循环迭代, tcp 服务会回调一次连接服务,当一个新的连接过来时。
requests 代表的是短时间活跃的操作。这些操作可以被表现依赖于 handle 上:写请求用来在 handle 上写数据。
其他基础知识
宏任务
宏任务是由 host 环境(runtime 如 Node.js 和浏览器)支持的,如 setTimeout 是由浏览器提供的。
- system events
- setTimeout
- requestAnimationFrame
- XHR
- …

微任务
微任务是由 V8 本身支持的。有以下任务
- Promise
- MutationObserver
- generator
- async/await
- …
微任务的存在是为了弥补宏任务不支持 real time 的场景,每执行完一次宏任务,都会执行完当前微任务队列里的所有微任务。

经典题目
What will happen in three scenes?
A. Endless loop and block the UI thread
B. Endless loop but not block the UI thread
C. Stack overflow
D. Works fine
// scene 1 === C Stack overflow
function fun1 (){
console.log("test fun1");
fun1()
}
fun1();
// scene 2 === A Endless loop and block the UI thread
function fun2 (){
console.log("test fun1");
return Promise.resolve().then(fun2)
}
fun1();
// scene 3 === D Works fine
function fun3(){
console.log("test fun3");
setTimeout(fun3, 0)
}
fun3()