美文网首页
setInterval&setTimeout的执行时机

setInterval&setTimeout的执行时机

作者: 薯条你哪里跑 | 来源:发表于2023-08-31 15:39 被阅读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

    相关文章

      网友评论

          本文标题:setInterval&setTimeout的执行时机

          本文链接:https://www.haomeiwen.com/subject/ouczmdtx.html