一、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都会被定义为kJavascriptTimerImmediate
。kJavascriptTimerImmediate
又是啥呢?,我们可以看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;
}
在看这个Timeout
在 lib/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
网友评论