
Node 的定时器延时上限是 2147483647 毫秒,2 的 31 次方减一,折合 24.8551 天。官方文档在 setTimeout 的 delay 参数那一栏写着:大于 2147483647 或小于 1 的延时,会被设成 1。超出去的值没有被截到上限,它被换成了 1 毫秒。这次量的是 _idleTimeout:每个延时建一个定时器,读 Node 实际存了什么,再用一个 60 毫秒的小定时器看它有没有提前触发。
文档那一条,量出来是 24.8551 天
上限本身落在 2147483647 毫秒,也就是 32 位有符号整数能表示的最后一个正整数。比它小一毫秒的 2147483646 存下来还是 2147483646。到 2147483648 就掉头了,_idleTimeout 变成 1,那个 60 毫秒的观察定时器还没到点,它已经跑完。
== 3. 逼近 32 位上限的那几个值 ==
传入值 说明 _idleTimeout 60ms 内触发 实际耗时
----------------------------------------------------------------------------------------
2147483646 上限减一 2147483646 否
2147483647 上限本身 2147483647 否
2147483647.5 上限加半毫秒 1 是 0.2 ms
2147483648 上限加一 1 是 1.4 ms
2147483649 上限加二 1 是 0.2 ms
4294967296 2^32 1 是 1.6 ms
2592000000 三十天 1 是 1.5 ms
31536000000 三百六十五天 1 是 1.6 ms
10000000000 一百亿毫秒 1 是 1.6 ms
换成天数看:2147483647 毫秒是 24.8551 天,三十天的 2592000000 和一年的 31536000000 都在表尾变成了 1。想写每月跑一次的定时任务,正好落在被砍掉的那一侧。
上限加半毫秒也算超了
文档还有一句:非整数的延时会被截断成整数。如果先截断再判上限,2147483647.5 应该被截成 2147483647,然后安安静静等 24.8 天。实测它的 _idleTimeout 是 1,说明上限这一关在取整之前就过了。正常范围内的 1000.4 和 1000.7 存下来还是带小数的原样。
== 2. 正常范围内的延时,对象里存下来的值 ==
传入值 说明 _idleTimeout 60ms 内触发 实际耗时
----------------------------------------------------------------------------------------
1.5 一点五毫秒 1.5 是 1.4 ms
999 差一毫秒到一秒 999 否
1000 一秒 1000 否
1000.4 一秒带小数 1000.4 否
1000.7 一秒带小数 1000.7 否
86400000 一天 86400000 否
604800000 一周 604800000 否
零和负数也被设成 1,警告名字不一样
0、0.4、0.5、-1、-100、-0.0001 存下来全是 1,60 毫秒内全部触发。文档那句「小于 1 会被设成 1」把它们盖住了。警告里能看出分支:负数打一条 TimeoutNegativeWarning,0 和不足 1 毫秒的小数什么都不打。
== 1. 零、负数、小数 ==
传入值 说明 _idleTimeout 60ms 内触发 实际耗时
----------------------------------------------------------------------------------------
0 零 1 是 1.3 ms
0 负零 1 是 1.3 ms
0.4 零点四毫秒 1 是 1.3 ms
0.5 半毫秒 1 是 1.3 ms
1 一毫秒 1 是 0.1 ms
-1 负一 1 是 1.9 ms
-0.5 负零点五 1 是 1.3 ms
-100 负一百 1 是 1.3 ms
-0.0001 负万分之一 1 是 1.3 ms
负数、正溢出和 NaN 的警告文本不一样。
== 4. 每一类警告的原文 ==
负一 TimeoutNegativeWarning: -1 is a negative number. / Timeout duration was set to 1.
上限加半毫秒 TimeoutOverflowWarning: 2147483647.5 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
上限加一 TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
上限加二 TimeoutOverflowWarning: 2147483649 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
2^32 TimeoutOverflowWarning: 4294967296 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
三十天 TimeoutOverflowWarning: 2592000000 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
三百六十五天 TimeoutOverflowWarning: 31536000000 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
一百亿毫秒 TimeoutOverflowWarning: 10000000000 does not fit into a 32-bit signed integer. / Timeout duration was set to 1.
到这一步收到 8 条警告
那句 does not fit into a 32-bit signed integer 就是全部原因。警告只写进 stderr,退出码不受影响,只收 stdout 的日志系统里看不出异常。
一条警告在进程里发几次
三类警告的重复行为不一样,文档里没写。负数警告整个进程只发一次,换成别的负数也不再发。NaN 警告同样只发一次。正溢出警告每次都发。
== 5. 警告的重复次数不一样 ==
负数三种各调一次 -> 新增 0 条
负数再调一次 -> 新增 0 条
NaN、非数字字符串、空对象各一次 -> 新增 1 条
NaN 再调一次 -> 新增 0 条
正溢出三种各调一次 -> 新增 3 条
正溢出再调一次 -> 新增 1 条
第一行是 0 条,不是写错了:负数警告在更早的实验里已经发过。
字符串一千能用,BigInt 直接抛错
延时不是数字的时候,先转一次再判断。字符串 "1000" 存下来是 1000 毫秒,"1e3" 也一样;非数字字符串和空对象转出来是 NaN,落到 1。BigInt 转不过去,直接抛 TypeError: Cannot mix BigInt and other types,这句报错说明延时在被使用前做过一次乘法转换。
== 6. 非数字的延时怎么处理 ==
传入值 说明 结果
----------------------------------------------------------------------------------------
undefined undefined _idleTimeout = 1 60 毫秒内触发 = 是
null null _idleTimeout = 1 60 毫秒内触发 = 是
true 布尔 true _idleTimeout = 1 60 毫秒内触发 = 是
false 布尔 false _idleTimeout = 1 60 毫秒内触发 = 是
1000 字符串一千 _idleTimeout = 1000 60 毫秒内触发 = 是
1e3 字符串科学计数 _idleTimeout = 1000 60 毫秒内触发 = 是
abc 非数字字符串 _idleTimeout = 1 60 毫秒内触发 = 是
NaN NaN _idleTimeout = 1 60 毫秒内触发 = 是
Infinity 正无穷 _idleTimeout = 1 60 毫秒内触发 = 是
-Infinity 负无穷 _idleTimeout = 1 60 毫秒内触发 = 是
1000 BigInt 一千 抛错 TypeError: Cannot mix BigInt and other types, use explicit conversions
[object Object] 空对象 _idleTimeout = 1 60 毫秒内触发 = 是
空数组 _idleTimeout = 1 60 毫秒内触发 = 是
从配置里读进来的字符串延时,能转成数字就照常生效,这比抛错更难查。
setInterval 走的是同一套校验
文档把 setTimeout 与 setInterval 的 delay 写在同一个参数说明里,实测两边一致:2147483647 原样留下,2147483648 变成 1。写错的周期值会让 setInterval 变成高频循环。
== 7. setInterval 有没有同样的上限 ==
setInterval(…, 1000 ) _idleTimeout = 1000
setInterval(…, 2147483647 ) _idleTimeout = 2147483647
setInterval(…, 2147483648 ) _idleTimeout = 1
setInterval(…, 2592000000 ) _idleTimeout = 1
setInterval(…, 31536000000 ) _idleTimeout = 1
nextTick 和 setImmediate 没有延时参数
这两个 API 不接受毫秒数,上面那条上限对它们不适用。差别在别的地方:nextTick 排在当前这次操作之后、进入下一个阶段之前;setImmediate 属于 check 阶段,在 poll 阶段结束后跑。把它们和 promise.then 放在同一个场景里排队,谁先谁后取决于代码跑在哪里。
== 8. nextTick 与 promise.then 谁先,看代码跑在哪里 ==
ESM 顶层(就是本文件) -> promise.then, nextTick
定时器回调里 -> nextTick, promise.then
I/O 回调里 -> nextTick, promise.then
await 之后 -> promise.then, nextTick
CommonJS 顶层(另起一个进程) -> nextTick, promise.then
前四行都在同一个进程里,差别只在调用的位置。ESM 顶层和 await 之后是 promise.then 在前,定时器回调与 I/O 回调里换成了 nextTick 在前。
== 9. setImmediate 与 setTimeout(0) 的先后,另起进程连跑 20 次 ==
setImmediate -> setTimeout0 18 次 / 20
setTimeout0 -> setImmediate 2 次 / 20
setImmediate 与 setTimeout(0) 的先后是另一回事。二十次里十八次是 setImmediate 先,两次反过来,放进 I/O 回调里则稳定。把它当成「一定比 setTimeout(0) 早」的保证,在主脚本里靠不住。
== 10. I/O 回调里再排一次 setImmediate 与 setTimeout(0) ==
I/O 回调里 -> setImmediate -> setTimeout0
十万次 nextTick 之后,0 毫秒定时器等了 11 毫秒
nextTick 队列不归事件循环的阶段管,排在当前操作之后马上清空。十万次递归排进去,后面的 0 毫秒定时器要等这一队走完才拿到事件循环。
== 11. 用 nextTick 排大队,0 毫秒的定时器被推后多久 ==
先排 100000 个 nextTick,再等一个 0 毫秒定时器
定时器实际等了 11.3 ms 才拿到事件循环
11 毫秒是这一台机器这一轮的量级,随负载会变。方向是确定的:延时参数写 0 也只是「尽快」,前面有多少活儿排队,它就得排在后面。
这条上限来自 32 位有符号整数,改动不了,只能绕开。我在长周期任务里不指望 setTimeout 撑满一个月,超过一天的部分改成按时段续期;真按天跑的东西交给系统调度更稳,代价是部署时多一份定时配置。