10

随着 Windows 系统接近 49.7 天的正常运行时间,内部 Windows 毫秒滴答计数器接近 2^32。在计算何时触发 setInterval 或 setTimeout 事件时,Internet Explorer 8 中的一个错误似乎存在算术溢出。例如,如果您在正常运行时间的第 49 天,并致电

setInterval(func, 86400000); // fire event in 24 hours

func 将立即被调用,而不是在 24 小时内。

如果将足够大的数字传递给 setInterval 或 setTimeout,则此错误可能会在 25 天正常运行时间(2^31 毫秒)后的任何时间发生。(不过,我只在第 49 天检查过。)

您可以通过在命令行中输入“net statistics server”来查看正常运行天数。

有解决方法吗?

4

3 回答 3

5

您可以通过使用包装器来解决该错误setTimeout

function setSafeTimeout(func, delay){
    var target = +new Date + delay;
    return setTimeout(function(){
        var now = +new Date;
        if(now < target) {
            setSafeTimeout(func, target - now);
        } else {
            func();
        }
    }, delay);
}

这仍然返回值,setTimeout因此如果没有遇到错误,clearTimeout仍然可以使用。如果clearTimeout需要防弹或您需要setInterval(并且可能clearInterval)您需要在问题上抛出更多代码,但在执行func保留之前验证足够时间的原则已经过去。

于 2010-12-04T17:27:57.897 回答
3

这个项目帮助解决了我一直在处理的应用程序中的一个主要问题,非常感谢您发布它。AdjustTickCount 实用程序是用于证明解决方案的出色且必不可少的工具,因此也对此表示赞赏。

该问题也影响 IE 7,它似乎也影响 IE 6,但后果更严重,因为浏览器停止响应,并且解决方案似乎也不适用于该版本。这些旧版本仍然有很多用户,特别是在商业/企业领域。

我没有发现鼠标左键是 Windows XP 中的一个因素,没有它就会出现问题。

如果超时延迟最多只有几秒钟,并且应用程序中设置的超时数量很少,那么前两个答案很好。如果需要更多和更长的超时,则需要做更多的工作来防止 Web 应用程序变得不可用。在使用诸如qooxdoo之类的框架的 Web 2.0 RIA 中,用户可能会使其运行数天,因此可能需要更多和更长的超时,而不是几个半秒的延迟来产生动画或其他短暂的效果。

第一个解决方案是一个好的开始,但是将下一个超时设置为target-now将再次导致该函数被立即调用,因为这仍然会使正常运行时间+延迟超过 2^32 毫秒,因此 JS 代码将旋转直到正常运行时间回绕到 0 (或用户杀死浏览器)。

第二种解决方案是一种改进,因为过早超时只会以 1 秒的间隔发生,直到现在正常运行时间环绕的 1 秒内,这允许其他代码查看但实验表明仍然足以使浏览器如果有足够的未决超时在播放,则无法使用。并且它仍然会持续到正常运行时间结束,所以如果请求的延迟足够长,用户仍然可能决定终止浏览器。

一个较少占用 CPU 的解决方案是将每个后续超时的延迟设置为前一个延迟持续时间的一半,直到该延迟小于 500 毫秒,此时我们知道正常运行时间的环绕点即将到来(< 1 秒)我们可以将下一个超时设置为,target-now这样过早的超时检查只会在进一步的少量循环后停止。达到这一点需要多长时间取决于原始延迟的时间长度以及正常运行时间环绕在setSafeTimeout被调用时的接近程度,但最终在 CPU 负载最小的情况下,应用程序将恢复正常行为,而用户不会经历任何长时间的减速。

像这样的东西:

function setSafeTimeout(func, delay) {
  var target = +new Date + delay;
  var newDelay = delay;
  var helper = function()
  {
    var now = +new Date;
    if (now < target)
    {
      newDelay /= 2; // halve the wait time and try again
      if(newDelay < 500) // uptime wrap around is imminent
      {
        newDelay = target-now; // go back to using original target
      }
      var handle = setTimeout(helper, newDelay);
      // if required record handle somewhere for clearTimeout
    }
    else
    {
      func();
    }
  };
  return setTimeout(helper, delay);
};

进一步细化:

我发现setTimeout()即使系统正常运行时间不接近 2^32 毫秒,有时也可以比预期提前几毫秒调用回调。在这种情况下,上述函数中使用的下一个等待间隔可能大于到原始目标的剩余时间,这导致等待时间比最初预期的要长。

以下是解决此问题的另一个版本:

function setSafeTimeout(func, delay) {
  var target = +new Date + delay;
  var newDelay = delay;
  var helper = function()
  {
    var now = +new Date;
    if (now < target)
    {
      var timeToTarget = target-now;
      newDelay /= 2; // halve the wait time and try again
      if(newDelay < 500 || newDelay > timeToTarget) // uptime wrap around is imminent
      {
        newDelay = timeToTarget; // go back to using original target
      }
      var handle = setTimeout(helper, newDelay);
      // if required record handle somewhere for clearTimeout
    }
    else
    {
      func();
    }
  };
  return setTimeout(helper, delay);
};
于 2011-01-07T19:30:37.293 回答
1

卡梅伦乔丹答案的变体:

function setSafeTimeout(func, delay) {
    var target = +new Date + delay;
    var helper = function() {
        var now = +new Date;
            if (now < target) {
                setTimeout(arguments.callee, 1000);
            } else {
                func();
            }
        }
    return setTimeout(helper, delay);
}

如果 IE8 处于错误状态,辅助函数的目的是每秒调用一次自身。

一个有用的测试实用程序是AdjustTickCount(不过,仅适用于 Windows XP)。例如,将新的滴答计数设置为 0xffff0000,将使您在滴答计数器翻转之前有 65 秒的错误行为。任何设置为 120 秒的计时器都不会正常触发。

此外,根据这篇文章,在 Windows XP 中,错误的 setTimeout 行为似乎与单击鼠标左键有关。

于 2010-12-06T21:42:36.813 回答