为什么 setTimeout 在 Node.js 中返回对象(以及为什么 setInterval 会导致你的应用崩溃)
Source: Dev.to
请提供您希望翻译的文章正文内容,我将按照要求保留链接、格式和代码块,仅翻译文本部分。
浏览器计时器 vs Node.js 计时器
在浏览器中,计时器是 Web API 的一部分。调用 setTimeout 会返回一个数字标识符:
const id = setTimeout(fn, 1000);
该数字仅仅是一个查找键。浏览器维护一个内部计时器表,并使用该数字在需要时取消计时器。由于浏览器已经运行了 UI 事件循环,计时器并不影响环境是否保持存活。
Node.js 是一个必须决定何时退出的进程
Node.js 通常作为服务器或 CLI 进程运行。与浏览器不同,它必须不断回答一个关键问题:
“还有没有需要我保持存活的工作?”
为了解答这个问题,Node 会跟踪 活动资源,例如:
- 打开的服务器
- 打开的套接字
- 文件描述符
- 定时器
如果没有活动资源,Node 就会退出。这也是 Node 定时器不仅仅是数字的关键原因。
为什么 setTimeout 在 Node.js 中返回对象
在 Node.js 中,setTimeout 返回一个 计时器句柄对象(类型为 NodeJS.Timeout)。该对象代表事件循环中一个真实、已注册的资源,并且可以:
- 参与生命周期跟踪
- 影响进程是否保持存活
- 暴露可以改变上述行为的方法
单纯的数值 ID 无法实现这些功能。
Source: …
ref() 与 unref() 的含义
默认行为:计时器是 “ref’d”
默认情况下,使用 setTimeout 或 setInterval 创建的每个计时器都是 ref’d。从概念上讲,这意味着:
“这个计时器很重要。不要让进程在它完成之前退出。”
setTimeout(() => {
console.log("done");
}, 10_000);
显式调用 ref() 是不必要的,因为这种行为已经是默认的。
unref():让计时器变为可选
调用 unref() 会改变计时器的角色:
const t = setTimeout(task, 10_000);
t.unref();
现在计时器会说:“如果我是唯一剩下的东西,就不要等我了。” 如果所有其他资源都已经消失,Node 会立即退出,计时器可能根本不会触发。这是有意为之,适用于后台或尽力而为的工作。
ref():撤销 unref()
ref() 方法的唯一作用是逆转 unref():
t.ref();
通常只有在库、可复用的基础设施代码,或计时器重要性会动态变化的场景中才会调用 ref()。在普通的应用代码里,手动调用 ref() 几乎总是没有必要的。
Source: …
为什么 setInterval 比 setTimeout 更危险
核心区别简单却关键:
setTimeout只运行一次并自行清理。setInterval会一直运行,除非显式停止。
生命周期影响
setTimeout:
- 注册一个定时器
- 触发一次
- 自动注销自身
- 不再保持进程存活
setInterval:
- 注册一个定时器
- 反复触发
- 永不注销自身
- 无限期保持进程存活
如果忘记了 setTimeout,问题会自行结束。若忘记了 setInterval,进程可能永远无法退出。
静默的生产环境 Bug
const interval = setInterval(() => {
collectMetrics();
}, 60_000);
在关闭期间,HTTP 服务器关闭,数据库连接关闭,所有真实工作已经完成——但进程仍因该 interval 被 ref 而挂起。为避免这种情况,必须清除或 unref 该 interval:
clearInterval(interval);
或
interval.unref();
重叠执行:另一个 setInterval 陷阱
setInterval 并不关心上一次执行是否已经完成:
setInterval(async () => {
await slowTask(); // 耗时 10 秒
}, 5_000);
这可能导致执行重叠,引发竞争条件、无限并发、内存增长以及数据库过载。
为什么递归 setTimeout 更安全
一种常见且更安全的模式是:
async function loop() {
await slowTask();
setTimeout(loop, 5_000);
}
loop();
这种写法保证:
- 不会重叠
- 节奏可预测
- 自动清理
- 关闭行为更安全
该模式更符合 Node 的生命周期模型。
最佳实践摘要
- Node 计时器返回对象,因为它们是真正的事件循环资源。
- 计时器默认是
ref()的。 - 对于后台或尽力而为的任务使用
unref()。 - 除非完全掌控其生命周期,否则避免使用
setInterval。 - 对于重复的异步工作,优先使用递归的
setTimeout。 - 在服务器代码中始终考虑关闭行为。
有问题吗?请在评论中提出!