Node.js进程执行完最后一行非阻塞代码后不退出的原因及场景
核心逻辑:Node.js进程何时退出
Node.js进程不会随便终止,只有当事件循环的所有阶段都没有待处理任务,且所有活跃的异步资源都被释放时,才会自动退出。定时器只是其中一种能让进程保持运行的异步资源,除此之外还有不少常见场景。
除定时器外,阻止进程退出的常见场景
1. 活跃的网络服务器/套接字
只要你启动了HTTP、TCP或UDP服务器(比如Fastify的listen),Node.js就会把对应的监听套接字注册到事件循环里,持续等待新的连接请求。只要服务器处于监听状态,这个套接字就是活跃资源,事件循环不会停,进程自然不会退出。
就像你提到的Fastify示例:不管你用不用await调用listen(),底层都会创建一个HTTP服务器并绑定端口。这个服务器会持有一个文件描述符(对应监听的套接字),Node.js会一直盯着这个套接字的连接事件,所以进程不会自己终止。除非你主动调用fastify.close()关闭服务器,释放这个套接字资源。
2. 未关闭的文件流/管道
如果打开了文件读写流、IPC管道但没正确关闭,这些资源会一直处于活跃状态,拖住进程不让它退出。比如:
const fs = require('fs'); // 创建读流但不关闭 const readStream = fs.createReadStream('big-file.txt');
这种情况下,流会保持打开状态,进程无法自动终止,必须手动调用readStream.close()或者等流的end事件触发后自动关闭。
3. 活跃的子进程
用child_process模块创建子进程后,如果没监听子进程的exit事件、没调用child.kill()终止它,父进程会一直等着子进程结束(或者保持IPC通道开放),导致父进程无法退出。
4. 全局事件监听器
如果给process、events.EventEmitter这类全局对象注册了事件监听器,只要这个事件源还可能触发事件,进程就会保持运行。比如:
// 注册了uncaughtException监听器,进程不会因为未捕获异常终止,也不会自动退出 process.on('uncaughtException', (err) => { console.error(err); });
不过如果是一次性的事件(比如once注册的监听器),触发后监听器会被移除,后续如果没有其他资源,进程会正常退出。
5. Worker线程
如果创建了Worker线程,主线程会一直运行,直到所有Worker线程都被终止或者关闭。只要有Worker线程在活跃,主线程的事件循环就不会停。
6. 未处理的Promise拒绝(Node.js 14+)
在Node.js 14及以上版本中,未处理的Promise拒绝默认会让进程终止,但如果你手动添加了unhandledRejection事件监听器,进程会继续运行下去。
再说说Fastify listen的细节
Fastify的listen()底层封装了Node.js的http.Server,调用后会完成端口绑定并启动监听。这个服务器实例会被Fastify内部持有,同时注册了connection、request等事件的监听器。
哪怕你不await这个方法,服务器依然会成功启动并进入监听状态——因为listen()的异步逻辑只是在通知你服务器启动完成,而服务器本身的监听动作已经同步完成了。此时Node.js的事件循环会持续监听服务器的套接字,所以进程不会退出。要让进程终止,必须主动调用fastify.close()关闭服务器,释放掉监听套接字这个活跃资源。
举个例子:
const fastify = require('fastify')(); fastify.get('/', (req) => req.send('Hi there')); // 不await,进程依然不会退出 fastify.listen({ port: 3000 }); // 5秒后关闭服务器,进程会自动退出 setTimeout(() => fastify.close(), 5000);
内容的提问来源于stack exchange,提问作者The Fool

