setInterval&setTimeout的执行时机

2023-08-31  本文已影响0人  薯条你哪里跑

一、chrome中setInterval&setTimeout执行时机

先看一段代码:

console.log(1)
setTimeout(()=>{
    console.log(2)
})
const i = setInterval(()=>{console.log(3)})
setTimeout(()=>{
    console.log(4)
    clearInterval(i)
})

此时输出1、2、4即第二个setTimeout的回调要比setInterval更先执行。

原因是在目前的 Chrome 里 setInterval 的最小延迟时间不是 0,而是 1,即便你写了 0,Chrome 也会改成 1,而 setTimeout 没有这个限制,所以 setTimeout 回调会先被推入任务队列且先执行,也就执行了 clearInterval,所以不会打印 3。

我们试一下:

console.log(1)
setTimeout(()=>{
    console.log(2)
})
const i = setInterval(()=>{console.log(3)})
setTimeout(()=>{
    console.log(4)
    clearInterval(i)
},0.9)

从代码上看,过了0.9ms之后会将该回调扔进任务队列并执行,此时输出1、2、4
我们将其改为 1

console.log(1)
setTimeout(()=>{
    console.log(2)
})
const i = setInterval(()=>{console.log(3)})
setTimeout(()=>{
    console.log(4)
    clearInterval(i)
},1)

由于上面说了setInterval默认也是1,那么就会按照代码书写的顺序优先将setInterval推进队列,此时输出为 1、2、3、4
chrome源码 其实setTimeout和setInterval都是在这一个函数里实现的,他俩通过single_shot区分,可以看到确实给setInterval的delay设定最小1ms

....
  // Clamping up to 1ms for historical reasons crbug.com/402694.
  // Removing clamp for single_shot behind a feature flag.
  if (!single_shot || !blink::features::IsSetTimeoutWithoutClampEnabled())
    timeout = std::max(timeout, base::Milliseconds(1));

  if (single_shot)
    StartOneShot(timeout, FROM_HERE);
  else
    StartRepeating(timeout, FROM_HERE);
  const char* name = single_shot ? "setTimeout" : "setInterval";
.....
结论:setInterval最小延迟是1ms,而setTimeout则是0ms

二、setTimeout在chrome中delay小于1ms时

  setTimeout(()=>{console.log(5)},5)
  setTimeout(()=>{console.log(4)},4)
  setTimeout(()=>{console.log(3)},3)
  setTimeout(()=>{console.log(2)},2)
  setTimeout(()=>{console.log(0)},0)
  setTimeout(()=>{console.log(1)},1)

输出:0、1、2、3、4、5 看其实是按照时间,没啥毛病
但是:

setTimeout(()=>{console.log(5)},5)
setTimeout(()=>{console.log(4)},0.4)
setTimeout(()=>{console.log(3)},0.3)
setTimeout(()=>{console.log(2)},2)
setTimeout(()=>{console.log(1)},1)
setTimeout(()=>{console.log(0)},0)

此时输出:4、3、0、1、2、5 (如果是safari输出 4、3、1、0、2、5)

说明当setTimeout的delay设置小于1ms时,不再根据等待时间将回调放入任务队列,这是咋回事呢?

mdn文档中针对setTimeout的delay参数描述如下:

If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, the next event cycle.

如果是0的话就会被“立即”执行,更准确的讲是在下次时间循环时;但是上面例子中写的 0.3 ,0.4并不是0啊,为啥没有按照常规操作不根据delay来执行呢?

我们看一下chrome这部分源码 chromium源码

//https://github.com/chromium/chromium/blob/main/base/token.h#L48
....
constexpr bool is_zero() const { return words_[0] == 0 && words_[1] == 0; }
....
//https://github.com/chromium/chromium/blob/100.0.4845.0/third_party/blink/renderer/core/frame/dom_timer.cc#L99
...
 // Select TaskType based on nesting level.
  TaskType task_type;
  if (timeout.is_zero()) {
    task_type = TaskType::kJavascriptTimerImmediate;
    DCHECK_LT(nesting_level_, kMaxTimerNestingLevel);
  } else if (nesting_level_ >= kMaxTimerNestingLevel) {
    task_type = TaskType::kJavascriptTimerDelayedHighNesting;
  } else {
    task_type = TaskType::kJavascriptTimerDelayedLowNesting;
  }
...

可以看到内部会判断delay如果是 0 开头的delay的TaskType都会被定义为kJavascriptTimerImmediatekJavascriptTimerImmediate又是啥呢?,我们可以看task_type.h这个文件,这里面记录了各个任务的优先级

...
  // https://html.spec.whatwg.org/multipage/webappapis.html#timers
  // For tasks queued by setTimeout() or setInterval().
  //
  // Task nesting level is < 5 and timeout is zero. 
  kJavascriptTimerImmediate = 72,
  // Task nesting level is < 5 and timeout is > 0.
  kJavascriptTimerDelayedLowNesting = 73,
  // Task nesting level is >= 5.
  kJavascriptTimerDelayedHighNesting = 10,
...

0.4ms、0.3ms是kJavascriptTimerImmediate类型任务,优先级是72;而其他 3ms 5ms等类型是kJavascriptTimerDelayedLowNesting优先级是73,这就是没有按照想象中的顺序执行的原因!

还有另外一个例子(chrome下):

例子1:
setTimeout(()=> console.log('111'),1000)
alert('aaa')
setTimeout(()=> console.log('333'), 1)    //输出 111,333
例子2:
setTimeout(()=> console.log('111'),1000)
alert('aaa')
setTimeout(()=> console.log('222'), 0.9) 
setTimeout(()=> console.log('333'), 1)  
 //在chrome输出 222,111,333 而且可观察到alert弹出过了1000ms之后在点击‘确定’,输出111前没有1000ms延迟,说明在alert时计时器也在同时计数
// 但在safari是222,333,111  而且可观察到alert弹出过了1000ms之后在点击‘确定’,输出111前会有1000ms延迟,说明在alert时计时器并没有计时

例子1:原因是alert执行时我们的1000已经开始计时,点击alter的‘确定’使其消失的时间大于1000ms所以输出111,333。当我们把1000改为3000时,输出即为333,111

例子2:按照上面说的结论setTimeout的delay设置小于1ms时会被判断timeout.is_zero()为true及立即执行,就可以很好理解这个例子: 执行alert后就会将下面setTimeout(()=> console.log('333'), 0.9) 的回调直接放进任务队列中立即执行,即使此时经过了1ms第三个settimeout和经过1000ms第一个settimeout也已经推入到任务队列了,但是无奈被小于1ms的插队了;

结论:chrome小于1ms的行为和大于等于1ms时不一致,小于1ms时行为与0ms一致均‘立即’执行,但是safari无论是否小于1ms行为都一致;当delay均小于1ms时,chrome和safari均是按照代码书写顺序来执行,这点是一致的

另外及时设置了小于等于1ms实际的最小延迟时间也是4ms,所以真正执行回调时至少有4ms延迟。源码

...
constexpr base::TimeDelta kMinimumInterval = base::Milliseconds(4);
...
 if (nesting_level_ >= kMaxTimerNestingLevel && timeout < kMinimumInterval)
    timeout = kMinimumInterval;
...


三、 settimeout在node中delay小于1ms时

setTimeout(()=>{
    console.log(1)
},1)
setTimeout(()=>{
    console.log(0.2)
},0.2)   
//输出 1  0.2   而chrome中确是0.2  1

node中的源码在 lib/timers.js

const {Timeout} = require('internal/timers');
function setTimeout(callback, after, arg1, arg2, arg3) {
  validateFunction(callback, 'callback');
  .....
  const timeout = new Timeout(callback, after, args, false, true);
  insert(timeout, timeout._idleTimeout);

  return timeout;
}

在看这个Timeoutlib/internal/timers.js中, 可以看到最小值就是1ms

class Timeout {
  // Timer constructor function.
  // The entire prototype is defined in lib/timers.js
  constructor(callback, after, args, isRepeat, isRefed) {
    after *= 1; // Coalesce to number or NaN
    if (!(after >= 1 && after <= TIMEOUT_MAX)) {
      if (after > TIMEOUT_MAX) {
        process.emitWarning(`${after} does not fit into` +
                            ' a 32-bit signed integer.' +
                            '\nTimeout duration was set to 1.',
                            'TimeoutOverflowWarning');
      }
      after = 1; // Schedule on next tick, follows browser behavior
    }
    ...
    ...
}

结论:

1.代码层面chrome中setInterval最小延迟是1ms,而setTimeout则是0ms
2.chrome中settimeout中delay小于1ms时和预期行为不符,是因为源码中小于1ms被定义为与0ms一样的‘立即’执行任务了。还有个小点setTimeout的delay是向下取整的即1.9ms和1ms等价、0.8ms和0ms等价
3.node中settimeout的delay小于1ms时会被修改为1ms

上一篇下一篇

猜你喜欢

热点阅读