setTimeout 提前调用函数

问题描述 投票:0回答:2

我的库有基于实时的测试用例,我注意到测试会随机失败并出现 1 毫秒的错误:

expect(received).toBeGreaterThanOrEqual(expected)

    Expected: >= 1000
    Received:    999

这似乎是由于 setTimeout 过早调用该函数所致。

所以我写了一个单独的测试脚本:

let last = Date.now()

setTimeout(next, 1000)

function next() {
  if (Date.now() - last < 1000) process.exit(1)
  last = Date.now()
  setTimeout(next, 1000)
}

在 Node.js v12.19.0、v14.15.3、v15.4.0 上,会随机失败:有时脚本可以继续运行,有时脚本很快就会退出。 这不仅发生在我的本地计算机上,还发生在 Github 的 CI 服务器上。

我的问题:这是一个错误吗?或者 setTimeout 的某种预期行为?或者

Date.now() - time
总是需要添加1毫秒?

更新:另请参阅https://github.com/nodejs/node/issues/26578

javascript node.js settimeout
2个回答
3
投票

更新:这里使用
git-bisect
是罪魁祸首:

2c409a285359faae58227da283a4c7e5cd9a2f0c is the first bad commit
commit 2c409a285359faae58227da283a4c7e5cd9a2f0c
Date:   Tue Aug 25 13:36:37 2020 -0600

    perf_hooks: add idleTime and event loop util
    
    Use uv_metrics_idle_time() to return a high resolution millisecond timer
    of the amount of time the event loop has been idle since it was
    initialized.
    
    Include performance.eventLoopUtilization() API to handle the math of
    calculating the idle and active times. This has been added to prevent
    accidental miscalculations of the event loop utilization. Such as not
    taking into consideration offsetting nodeTiming.loopStart or timing
    differences when being called from a Worker thread.
    
    PR-URL: https://github.com/nodejs/node/pull/34938

这似乎是一个错误,而不是预期的行为。我会投票反对总是添加 1ms,因为行为不一致。 (但是,它是否会早于 1 毫秒?我没有观察到超过 1 毫秒)您可以通过以下方法解决该问题:

const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
  let o;
  const when = Date.now() + ms,
    check = ()=> {
      let t = when - Date.now();
      if (t > 0) Object.assign(o, origSetTimeout(check, t));
      else f(...args);
    };
  return o = origSetTimeout(check, ms);
};

即使在解决问题时也可以

clearTimeout()

这里是一个浏览器代码,它模拟该问题并每 3 秒更换一次解决方法:

// Simulate the problem
const realOrigSetTimeout = setTimeout;
setTimeout = (func, ms, ...args) => realOrigSetTimeout(func, ms - Math.random(), ...args);

const ms = 200;
let when = Date.now() + ms;
setTimeout(next, ms);

function next() {
  let now = Date.now();
  setTimeout(next, ms);
  console.log(now < when ? 'premature' : 'ok');
  when = now + ms;
}

function workAround() {
  console.log('Applying workaround');

  const origSetTimeout = setTimeout;
  setTimeout = (f, ms, ...args) => {
    let o;
    const when = Date.now() + ms,
      check = ()=> {
        let t = when - Date.now();
        if (t > 0) Object.assign(o, origSetTimeout(check, t));
        else f(...args);
      };
    return o = origSetTimeout(check, ms);
  };

  setTimeout(_=>{
    console.log('Removing workaround');
    setTimeout = origSetTimeout;
    setTimeout(workAround, 3000);
  }, 3000);
}
setTimeout(workAround, 3000);

下面是一个 Nodejs 代码,它将清楚地显示问题(点中的“p”),并在按 Enter 后应用解决方法。

'use strict';

const ms = 1;
let when = Date.now() + ms;
setTimeout(next, ms);

function next() {
  let now = Date.now();
  setTimeout(next, ms);
  process.stdout.write(now < when ? 'p' : '.');
  when = now + ms;
}

process.stdin.on('readable', _=> {
  console.log('enabling workaround');
  const origSetTimeout = setTimeout;
  setTimeout = (f, ms, ...args) => {
    let o;
    const when = Date.now() + ms,
      check = ()=> {
        let t = when - Date.now();
        if (t > 0) Object.assign(o, origSetTimeout(check, t));
        else f(...args);
      };
    return o = origSetTimeout(check, ms);
  };
});

0
投票

定时器在底层以毫秒精度运行,并且(截至撰写本文时)在 Node.js 中始终如此,因此 1000 毫秒的定时器始终有可能仅在(例如)999.95 毫秒后运行。由于

Date.now()
也是毫秒精度,因此相同的持续时间可能仅显示为 999ms。

例如,假设计时器可能在纪元后 0.55 毫秒启动,并在纪元后 1000.45 毫秒运行。这是 999.9 毫秒的持续时间,毫秒精度四舍五入到 1000 毫秒,这就是计时器运行的原因。但

Date.now()
是从时代开始衡量的。因此,就
Date.now()
而言,计时器设置为纪元后 1 毫秒(从 0.55 毫秒开始舍入),并在纪元后 1000 毫秒(从 1000.45 毫秒开始舍入)运行,持续时间为 999 毫秒。

这是一个错误还是仅仅是一个怪癖是值得商榷的。 (我认为这是一个错误,但理解不这么认为的人。)

我猜这只是为了通过测试,而不是实际上可能影响用户的事情。如果是这样,您可以安全地将期望更改为

>= 999
。另一种需要更多工作的选择是使用
process.hrtime()
process.hrtime.bigint()
来获取更高精度的时间戳并相应地进行舍入。

© www.soinside.com 2019 - 2024. All rights reserved.