news 2026/7/30 11:30:03

从一道经典面试题洞悉 NodeJS 事件循环的核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从一道经典面试题洞悉 NodeJS 事件循环的核心原理

1 关于 nextTick 回调和 Promise 回调的先后问题

欧莱利曾于 2020 年出版发行过一本介绍分布式系统部署的奇书《Distributed Systems with Node.js》,仔细看过的朋友几乎都建议不要看中文版,理由是翻译常常不能准确传达作者本意,并且书中的实例有些也是错的。例如下面这道本想考察事件循环各阶段执行顺序的经典面试题:

// package.json: {"type": "commonjs"}// test-cjs.jsconst{readFile}=require('node:fs')constlg=console.logsetImmediate(()=>lg(1))Promise.resolve().then(()=>lg(2))process.nextTick(()=>lg(3))readFile(__filename,()=>{lg(4)setTimeout(()=>lg(5))setImmediate(()=>lg(6))process.nextTick(()=>lg(7))})lg(8)// -> 8 3 2 1 4 7 6 5

第一次实测结果确实和书里一样:

【图1】面试题实测结果

但要是改为ESM模块化规范,结果确实不一样:

// package.json: {"type": "module"}// test-esm.jsimport{readFile}from'node:fs'import{fileURLToPath}from'node:url'const__filename=fileURLToPath(import.meta.url)constlg=console.logsetImmediate(()=>lg(1))Promise.resolve().then(()=>lg(2))process.nextTick(()=>lg(3))readFile(__filename,()=>{lg(4)setTimeout(()=>lg(5))setImmediate(()=>lg(6))process.nextTick(()=>lg(7))})lg(8)// -> 8 2 3 1 4 7 6 5

二者差异主要体现在Promise回调和process.nextTick()回调的执行顺序上:

  • CJS模块化:process.nextTick回调优先;
  • ESM模块化:Promise回调优先。

Node.js官网一查,官网还为二者的区别 单开了一页 详细解释,可见这个问题当时有多热门。

概括来说,在node事件循环的同一轮迭代周期内,process.nextTick()对应的执行队列被清空后,Promise所在的微任务队列才会被立即清空;

本来ESM版实现应该和CJS版一样,都遵循process.nextTick()回调先于Promise回调

但是,在最新的Node.js版本中,ESM模块已经作为微任务队列的一部分在进行处理了,只能等到当前队列清空后再到下一轮迭代周期去触发nextTick回调所在的执行队列,因此执行顺序看上去是反着的;但逻辑上它们应该属于不同的事件循环的迭代周期:Promise队列在上一轮,nextTick队列在下一轮。

所以,这又引出了另一个很坑的知识点:既然执行顺序上并不等效,为什么官方还要让大家优先考虑 queueMicrotask() 而不是 process.nextTick() 呢?

答:因为出于可移植性和稳定性的考虑:queueMicrotask() API更适用于存在多种JavaScript平台环境的场合,毕竟queueMicrotask()的队列在Node.js底层是由V8模块维护的,而process.nextTick()则是Node.js自己维护的:

【图2】process.nextTick 与 queueMicrotast 在 Node.js 底层分属不同的模块进行维护

如果即不考虑可移植的问题,也不在意刚刚提到的执行顺序的差异,其余情况下二者效果才是差不多的。

最后需要注意的是,queueMicrotask()只接收一个参数,因此无法像process.nextTick()那样从第二个参数起,依次传入第一参数那个回调所需的参数,只能通过闭包或.bind()绑定参数(L6):

functiondeferred(a,b){console.log('microtask',a+b);}console.log('start');queueMicrotask(deferred.bind(undefined,1,2));console.log('scheduled');// Output:// start// scheduled// microtask 3

与第六行等效的nextTick版本为:process.nextTick(deferred, 1, 2)

2 关于 nextTick 引入的递归调用问题

因为一些历史遗留问题,process.nextTick()的诞生晚于setImmediate()。设计人本想让setImmediate()按字面意思,在Node.js事件循环的轮询(Poll)阶段完成后,立即执行紧随其后的检查(Check)阶段内的回调逻辑,即setImmediate()。后来却发现这样还不够及时,最好能在每个阶段完成回调后,立刻执行一些回调,而不急于转到事件循环的下一个阶段,于是才有了process.nextTick()这个“补丁”。这样一来,明明写着Immediate(立刻、马上)的API从实际效果来看并没有那么立刻;反而是让标着nextTick(下一刻、下个迭代)的API抢了风头。所以原书作者才加了一段注释:

  1. A “tick” refers to a complete pass through the event loop. Confusingly,setImmediate()takes a tick to run, whereasprocess.nextTick()is more immediate, so the two functions deserve a name swap.

也难怪网友们吐槽翻译的质量不高,原文那个诙谐玩味的腔调完全丧失了:

注3:tick 指的是一个完整的事件循环。令人困惑的是,setImmediate()需要经过一个循环周期,而process.nextTick()则更快地运行,因此这两个函数的名称其实应当互换。

言归正传。正是因为这个process.nextTick()补丁引入了递归调用的风险,比如回调函数就写成process.nextTick,这样整个代码就会在本阶段回调已清空、下个阶段回调未开始的临界状态一直空转,此时process.nextTick()对应的执行队列将永远无法清空,让下一阶段的正常回调等到肝肠寸断,直接“饿死”后续的I/O操作。

为什么这么明显的Bug还要这样设计呢?因为Node.js中的API的设计风格优先级更高,该风格建议优先处理本阶段出现的错误,然后再开始下个阶段。因此process.nextTick()严格意义上讲并不属于事件循环,无论当前位于事件循环哪个阶段,执行完回调逻辑后都会先清空process.nextTick()队列中的回调逻辑,让错误提前在回调中被解决。

那么设计者是如何化解递归调用风险的呢?答案都在函数签名里:process.nextTick(callback[, ...args]),即在process.nextTick()中支持传入剩余参数...args,作为运行callback回调函数时的实际参数,具体用法如下:

functionapiCall(arg,callback){if(typeofarg!=='string'){returnprocess.nextTick(callback,newTypeError('argument should be string'));}}

上述代码中的Error实例对象会被当作callback的第一参数传入回调逻辑中,这样就绕开了无限递归的坑。

3 事件循环各阶段排列顺序的最新变更

原书介绍在事件循环各阶段的排列顺序如下:

【图3】原书给出的 Node.js 事件循环的各个阶段

这在 2020 年和大家理解的顺序是有出入的;戏剧性的是,自从 2023 年 4 月Node.js v20发布后,即从libuv v1.45.0开始,Timers定时器阶段被调整到了Poll轮询阶段的后面,当年那个写法貌似和最新版的顺序【神同步】了(虽然不完全相同):

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

变更说明 原文如下:

Starting with libuv 1.45.0 (Node.js 20), timers are run after thepollphase in each event loop iteration. In earlier versions, timers were run before polling. To preserve backwards compatibility, libuv 1.45.0 still runs timers once before entering the event loop . This change can affect the timing ofsetImmediate()callbacks and how they interact with timers in certain scenarios.
从 libuv 1.45.0(Node.js 20)开始,定时器在每次事件循环迭代的轮询阶段之后执行。在早期版本中,定时器是在轮询之前执行的。为了保持向后兼容性,libuv 1.45.0 仍会在进入事件循环之前执行一次定时器。此变更可能会影响setImmediate()回调的计时及其在某些场景下与定时器的交互方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/30 11:29:52

如何快速掌握TranslucentTB:Windows任务栏透明化的终极指南

如何快速掌握TranslucentTB&#xff1a;Windows任务栏透明化的终极指南 【免费下载链接】TranslucentTB A lightweight utility that makes the Windows taskbar translucent/transparent. 项目地址: https://gitcode.com/gh_mirrors/tr/TranslucentTB 想要让Windows桌面…

作者头像 李华
网站建设 2026/7/30 11:29:14

从经纬度到球面距离:Haversine公式原理与多语言实现指南

1. 项目概述&#xff1a;从“两点之间直线最短”到“地球是个球” 我们从小就知道“两点之间&#xff0c;直线最短”。这个几何公理在平面地图上看起来无比正确&#xff0c;但当我们把目光投向真实世界&#xff0c;尤其是需要跨越成百上千公里时&#xff0c;这个简单的真理就遇…

作者头像 李华
网站建设 2026/7/30 11:24:46

3步解锁RPG Maker加密资源:从新手到专家的完整解密指南

3步解锁RPG Maker加密资源&#xff1a;从新手到专家的完整解密指南 【免费下载链接】RPG-Maker-MV-Decrypter You can decrypt RPG-Maker-MV Resource Files with this project ~ If you dont wanna download it, you can use the Script on my HP: 项目地址: https://gitcod…

作者头像 李华
网站建设 2026/7/30 11:24:31

ICMP协议深度解析:从网络故障排查到安全攻防实战

1. 项目概述&#xff1a;从一次“网络不通”的排查说起 前几天&#xff0c;一个刚入行的同事跑过来问我&#xff0c;说他负责维护的一个内部服务突然访问不了了&#xff0c;ping了一下目标服务器的IP地址&#xff0c;结果返回了一串“Destination Host Unreachable”。他一脸懵…

作者头像 李华
网站建设 2026/7/30 11:23:25

大模型学习资源包:从基础到前沿的AI实践指南

1. 项目概述&#xff1a;大模型学习资源包的定位与价值 这个资源包本质上是一个面向AI从业者和学习者的结构化知识库&#xff0c;它区别于市面上零散的教程集合。我整理这套资源的初衷&#xff0c;是发现很多学习者在接触大模型时存在三个典型痛点&#xff1a;一是学习路径不清…

作者头像 李华
网站建设 2026/7/30 11:22:28

KMS智能激活方案:开源工具的完整实战指南

KMS智能激活方案&#xff1a;开源工具的完整实战指南 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO KMS_VL_ALL_AIO是一款高效的开源智能激活脚本&#xff0c;能够为Windows系统和Office办公套…

作者头像 李华