有关定时器到期时间取值为负的问题
标准中的定义
在 HTML Living Standard
文档的 8.6 Timers
一节中定义了有关延时(delay)参数的取值说明:
If timeout is less than 0, then set timeout to 0. (如果超时时间比 0 小,那么设置超时时间我 0 )
下面是基于标准定义的具体实践:
setTimeout(console.log, -5000, 'Hello world!')
可以发现主流浏览器对标准都有很准确的实现。
最大延时值
包括 IE, Chrome, Safari, Firefox 在内的浏览器其内部以 32 位带符号整数存储延时参数。这就会导致如果一个延时(delay)超出 -2^31-1
~ 2^31
的安全范围内,就会因为溢出导致定时器被立即执行。
setTimeout(console.log, Math.pow(2, 31) + 1, 'timer 1')
setTimeout(console.log, Math.pow(2, 31) - 1, 'timer 2')
在实际的执行中可以发现 timer1
确实被立即执行并打印输出了结果,而 timer2
则顺利的进入了延时等待执行队列,这一结果非常符合我们的预期。
但是万事并无绝对,现在让我们观察下面的代码:
var now = Date.now();
var than = - Math.pow(2, 31) - 1;
var diff = 3 - now;
console.log(diff > than);
setTimeout(console.log, diff, 'Hello world!')
执行上面代码, diff > than
输出 false
说明延时取值已经不在安全取值范围内了,并且按照上述的描述以及标准中关于比 0 小的定义,当前定时器理应被立即执行,但实际上定时器并没有被立即执行,反而正常创建并返回了定时器 ID,这正是说明了定时器已经在延时等待执行了。
💡 实际上再多次的尝试后,只发现了负值可能会导致超出取值范围后并没有被立即执行。
源码中的表现
为了一探究竟,带着 3-now
这个特殊的值,我翻阅了 Mozilla Firefox
浏览器有关 setTimeout
实现的部分源码。
下面贴出最关键的部分,也是导致这一问题产生的诱因部分。
int32_t WorkerPrivate::SetTimeout(JSContext* aCx, TimeoutHandler* aHandler,
**int32_t aTimeout,** bool aIsInterval,
Timeout::Reason aReason, ErrorResult& aRv) {
auto data = mWorkerThreadAccessible.Access();
MOZ_ASSERT(aHandler);
// Reasons that doesn't support cancellation will get -1 as their ids.
int32_t timerId = -1;
if (aReason == Timeout::Reason::eTimeoutOrInterval) {
timerId = data->mNextTimeoutId;
data->mNextTimeoutId += 1;
}
// See if any of the optional arguments were passed.
**aTimeout = std::max(0, aTimeout);**
newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout);
newInfo->CalculateTargetTime();
}
上面代码中已将最关键的两个地方进行了加粗标识。可以看到我们设定的延时参数取值与我们之前的描述完全一致,即接收一个 32 位带符号整数,然后通过调用 std::max
方法进行取值,最小值只会返回 0(这也是对标准的实现)。
带着疑问,如果 std:max(aTimeout, 0)
的工作正常,返回值也符合预期结果,那么理论上不应该会出现 3-Date.now()
的值不会被立即执行的问题,为了更好的排查问题,我选择在一个 c++
的在线环境来着重测试 std:max
方法的行为于结果。
然后将以下代码粘贴到在线编辑器中运行:
#include <iostream>
using namespace std;
int main()
{
int32_t aTimeout = 100;
try{
aTimeout = -1648783802574;
}catch(const char *error){}
cout << "Hello World";
cout << aTimeout;
return 0;
}
最终,我们在输出的结果中,终于发现了产生这一问题的根本原因:
Hello World
483639090
main.cpp: In function ‘int main()’:
main.cpp:9:20: warning: overflow in conversion from ‘long int’ to ‘int32_t’ {aka ‘int’} changes value from ‘-1648783802574’ to ‘483639090’ [-Woverflow]
9 | aTimeout = -1648783802574;
| ^~~~~~~~~~~~~~
可以发现最终的原因是在 aTimeout
被重新赋值时产生了类型范围溢出,然后 c++ 自动进行了转换,将 -1648783802574 的溢出转换结果 483639090 重新赋值给了 aTimeout
变量,而 483639090 恰好又小于 2^31
次方,因此最终的结果便是定时器正常的进入了定时延迟执行队列中,更明确点来说就是会在 483639090 / 1000 / 60 / 60 / 24 = 5.6
天后才被执行。
转载自:https://juejin.cn/post/7241835342897365053