setInterval&setTimeout的执行时机

一、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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容