先记住这个答案
exit 表示子进程已经结束,可以取得退出码或结束信号,但相关标准流此时可能仍未关闭。close 表示进程结束且子进程的 stdio 流已经关闭;它在 exit 之后,或者在无法启动时的 error 之后到达。收集完整输出的任务通常应以 close 作为收尾条件,同时监听 error 处理启动失败。若还有其他进程共享管道,输出关闭可能晚于直接子进程退出,不能在 exit 回调里就假定全部数据已到齐。
- exit 对应进程执行结束
- close 还包含标准流已经关闭
- 启动失败要监听 error,不能只等 exit
操作系统进程与管道不是同一资源
子进程退出后,管道里可能仍有尚未交给父级处理的数据。父进程读取缓冲和关闭流有自己的时序,因此在 exit 里立即拼接当前收到的片段,可能漏掉最后一部分输出。
如果子进程启动了共享 stdout 的后代,直接子进程退出后,后代还可能继续持有写端。这个例子说明不能从一个 PID 已结束推断整条输出通道已经关闭,也解释了某些 close 比预期更晚的现象。
收集输出时把交付放到 close
下面固定子程序输出短文本,同时记录 data、exit 和 close。片段数量不是协议,一次 data 不保证就是一行;测试只要求最终文本完整,并检查 close 位于 exit 之后。
真实工具还应持续消费 stderr,避免另一条管道积压。示例明确忽略 stderr,以便集中说明事件顺序;需要诊断错误时应为 stderr 设计限量收集或日志去向,不能无意丢弃重要失败信息。
import { spawn } from 'node:child_process';
export function observeCompletion() {
return new Promise<{ text: string; events: string[]; code: number | null }>((resolve, reject) => {
const child = spawn(process.execPath, ['-e', 'process.stdout.write("ready")'], {
stdio: ['ignore', 'pipe', 'ignore']
});
const chunks: Buffer[] = [];
const events: string[] = [];
child.stdout!.on('data', (chunk: Buffer) => {
chunks.push(chunk);
events.push('data');
});
child.once('error', reject);
child.once('exit', () => events.push('exit'));
child.once('close', code => {
events.push('close');
resolve({ text: Buffer.concat(chunks).toString('utf8'), events, code });
});
});
}这段收集方式只适用于示例中受控的短输出。面对未知工具,应加入总字节上限或改为流式处理;事件顺序的验证也不应强行规定 data 相对 exit 的所有细节。
统一错误与结束状态,避免重复通知
启动失败可能先触发 error,再进入 close,而不经历正常 exit。若 error 和 close 都调用同一个业务结束回调,应确保只结算一次;Promise 只结算一次并不会自动阻止监听器内其他副作用重复执行。
正常结束通常有退出码,因信号结束则需要查看 signal。不要用简单的真值判断把 null 当成成功,也不要只判断 stderr 是否为空;成功条件应按目标程序协议和退出状态明确表达。
容易答错的地方
- 在 exit 中立刻返回累计输出
- 进程结束和输出流关闭是两个时刻,最后的数据可能还在传递。应把需要完整结果的交付放到 close,或者等待明确的流完成条件,并保留启动错误处理。
- 只要 close 到达就宣布命令成功
- close 说明相关生命周期走到结束,不说明退出码为零或产物可用。必须检查启动错误、退出状态和业务协议;失败程序同样会关闭流,不能把资源收尾事件当成成功事件。
面试官还会怎么问?
监听 stdout 的 end 能替代 close 吗?
它只描述这一条可读流结束,不覆盖 stderr、进程退出状态或启动错误。对于只处理一条数据流的局部逻辑可能有用,但完整子进程任务仍需要把这些条件组合起来,而不是只等一个流事件。
发送 kill 后应该等哪个事件确认结束?
发送信号只表示提出终止请求,仍需观察实际退出与流关闭。对于要回收配额或交付输出的任务,应等到相应结束条件满足;有后代进程时还要根据进程树管理策略确认它们的状态。
为什么子进程退出后 close 迟迟不到?
检查是否有后代继承了管道、父级是否正确处理各条流,以及实际任务是否仍持有相关资源。先确认具体 PID 和文件描述符关系,不要仅凭 exit 日志就断言 Node 漏发了事件。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。