likes
comments
collection
share

有关定时器到期时间取值为负的问题

作者站长头像
站长
· 阅读数 45

标准中的定义

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 实现的部分源码。

hg.mozilla.org/mozilla-cen…

下面贴出最关键的部分,也是导致这一问题产生的诱因部分。

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 方法的行为于结果。

C++ 在线工具 | 菜鸟工具

然后将以下代码粘贴到在线编辑器中运行:

#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
评论
请登录