在 JavaScript 的所有宿主环境中,无论是浏览器还是 Node.js,事件循环机制都不是 ECMAScript 的语言规范定义的。浏览器中的事件循环是根据 HTML 标准实现的,而 Node.js 中的事件循环则是基于 libuv 实现的。
libuv 是一个用 C 语言实现的高性能解决单线程非阻塞异步 I/O 的开源库,本质上它是对常见操作系统底层异步 I/O 操作的封装。在 nodejs 底层,Node API 的实现其实就是调用的它。
我们知道浏览器事件循环中执行异步任务的其他线程是由浏览器本身提供的,多线程调度是由渲染主线程完成的。而在 nodejs 中,这都是 libuv 完成的。
几乎每个 Node API 都有异步执行版本,libuv 直接负责它们的执行,libuv 会开启一个线程池,主线程执行到异步操作后,libuv 就会在线程池中调度空闲线程去执行,可以说 libuv 为 nodejs 提供了整个事件循环功能。
Node.js 中的 Event Loop
与在浏览器中一样,在 nodejs 中 JS 最开始在主线程上执行,执行同步任务、发出异步请求、规划定时器生效时间、执行 process.nextTick 等,这时事件循环还没开始。
在上述过程中,如果没有异步操作,代码在执行完成后便直接退出。如果有,libuv 会把不同的异步任务分配给不同的线程,形成事件循环。在同步代码执行完后,nodejs 便会进入事件循环,依次执行不同队列中的任务。libuv 会以异步的方式将任务的执行结果返回给 V8 引擎,V8 引擎再返回给用户
Nodejs 事件循环中的消息队列共有 8 个,若引用之前宏队列、微队列的说法,具体可划分为:
宏队列
- timers(重要)
- pending callback
- 调用上一次事件循环没在 poll 阶段立刻执行,而延迟的 I/O 回调函数
- idle prepare
- 仅供nodejs内部使用
- poll(重要)
- check(重要)
- close callbacks
- 执行所有注册 close 事件的回调函数
微队列
- nextTick
- Promise
timers
timers,也就是计时器队列,负责处理 setTimeout 和 setInterval 定义的回调函数。
值得注意的是,不管在浏览器中还是 nodejs 中,所有的定时器回调函数都不能保证到达时间后立即执行。一是因为从计算机硬件和底层操作系统来看,计时器的实现本身就是不精准的,二是因为 poll 阶段对 timers 阶段的深刻影响。因为在没有满足 poll 阶段的结束条件前,就无法进入下一次事件循环的 timers 阶段,即使 timers 队列中已经有计时器到期的回调函数。
pool
poll 称为轮询队列,该阶段会处理除 timers 和 check 队列外的绝大多数 I/O 回调任务,如文件读取、监听用户请求等。
事件循环到达该阶段时,它的运行方式为:
- 如果 poll 队列中有回调任务,则依次执行回调直到清空队列
- 如果 poll 队列中没有回调任务
-
- 若其他队列中后续可能会出现回调任务,则一直等待,等其他队列中后续的回调任务来临时,结束该阶段,开启下一次事件循环
- 若等待时间超过预设的时间限制,也会自动进入下一次事件循环
- 若其他队列中后续不可能再出现回调任务了,则立即结束该阶段,并在本轮事件循环完成后,退出 node 程序
poll 阶段的超时时间在进入 poll 阶段之前计算。
check
check 称为检查队列,负责处理 setImmediate 定义的回调函数。
setTimeout 和 setImmediate 的不同之处在于,每次执行到 timers 队列时,定时器观察者内部会去检查代码中的定时器是否超过定时时间,而 setImmediate 则是直接将回调任务加入到 check 队列。
所以总的来说,setImmediate 的执行效率要远高于 setTimeout,于是也就出现了下面无法预测输出结果的情况:
setTimeout(() => {
console.log('setTimeout');
}, 0)
setImmediate(() => {
console.log('setImmediate');
})
// 上述代码是无法预测先输出那个的
// 因为即使 setTimeout(xxx, 0),在计算机运算慢的情况下也不能立刻加入 timers 队列
对于微队列的 nextTick 和 Promise,严格意义上讲也不属于事件循环。在事件循环中,每次打算进入下个阶段之前,必须要先依次反复清空 nextTick 和 promise 队列,直到两个队列完全没有即将要到来的任务的时候再进入下个阶段。
我们可以通过 process.nextTick() 将回调函数加入 nextTick 队列,和通过 Promise.resolve().then() 将回调函数加入 Promise 队列,且 nextTick 队列的优先级还要高于Promise 队列,所以 process.nextTick 是 nodejs 中执行最快的异步操作。