Node.js系列:事件驱动的核心机制事件循环
❤️ 事件循环:处理异步操作的机制
目的:使Nodejs能够在单线程环境下高效运行
一、事件循环运行机制:依赖libuv库
事件循环机制是基于libuv库(一个多平台的异步I/O的库)构建的;
libuv为Nodejs提供了高效的事件驱动的I/O操作能力,libuv负责在底层进行实际的操作调度,当这些操作完成时,通过事件循环将对应的回调函数在合适的阶段进行调用;事件循环依赖kibuv实现高效的异步操作
比如在定时器管理方面,libuv 提供了精准的定时器机制,让事件循环能够准确地在合适的时间执行定时器回调函数(像
setTimeout
和setInterval
相关的回调)。在非阻塞 I/O 操作上,事件循环借助 libuv 可以在等待 I/O 完成的同时处理其他事务,避免了线程的大量阻塞,提高了程序的整体性能。
二、事件循环的6个阶段:每个阶段都对应一个任务队列
当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段
1. 定时器Timers阶段:执行setTimeout 和setInterval的回调函数
当设定的时间到达后,回调函数会被添加到 定时器阶段的任务队列中。(定时任务不一定按照设定的时间执行)
2.I/O回调阶段:主要用于处理各种I/O操作(如文件读取,网络请求等)完成后的回调函数
当一个I/O操作完成后, 其对应的回到函数就会被添加到这个任务队列中; 比如fs.readFile,文件读取完成后的回调函数就会在这个阶段会执行
3.闲置阶段:这是一个内部使用的过渡阶段
- 主要用于一些内部操作和准备工作,一般开发者很少直接涉及这个阶段的具体操作
4.轮询(Poll)阶段:事件循环的关键,主要有两个功能
- 等待新I/O事件到来
- 处理定时器到期后的任务(如果定时器阶段没来得及处理)
如果没有新的I/O事件并且定时器也没有到期任务,这个阶段会阻塞等待
5.检查(check)阶段:主要用于执行setImmediate的回调函数
- 在当前轮询阶段结束后立即执行
6.关闭事件回调阶段:TPC服务器对象关闭时,对应的关闭回调函数
- 例如关闭一个服务器套接字段后,用于清理资源等的关闭回调函数会在这个阶段被调用;
- 如:socket.on('close', ...)
三、任务队列和执行顺序
微任务:
- process.nextTick: 会在当前操作完成后立即执行,在微任务之前执行
- promise.then
- queueMicrotask():是标准的微任务
宏任务:
- setTimeout、setInterval
- IO事件
- 检查阶段的setImmediate
- 关闭事件
执行顺序:
-
nextTick
microtask queue -
other microtask
queue -
timer
queue -
poll
queue -
check
queue -
close
queue
微任务会在当前执行栈为空的时候立即执行,宏任务会根据事件循环的阶段顺序来执行
其他:
queueMicrotask 与 process.nextTick 的区别?
-
process.nextTick
会在当前操作完成后立即执行,甚至在事件循环的下一个阶段开始之前,而且在微任务之前执行。 -
queueMicrotask
是标准的微任务,会在当前事件循环的微任务队列中等待,在当前执行上下文的同步代码和process.nextTick
之后,但在宏任务之前执行。
setTimeout
与setImmediate
的输出顺序
- 遇到
setTimeout
,虽然设置的是0毫秒触发,但实际上会被强制改成1ms,时间到了然后塞入times
阶段; - 先进入
times
阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout
条件,执行回调,如果没过1毫秒,跳过 - 跳过空的阶段,进入check阶段,执行
setImmediate
回调
这里的关键在于这1ms,如果同步代码执行时间较长,进入
Event Loop
的时候1毫秒已经过了,setTimeout
先执行,如果1毫秒还没到,就先执行了setImmediate