Skip to main content

关于 setTimeout 的一些小故事

· 5 min read
VisualDust

@lideming曾经向我举过几个setTimeout的有趣例子。我先前不是很了解 JavaScript 的并发模型和事件循环,这些例子对我来说都算得上是巨坑无比了。

setTimeout 和事件循环

函数 setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其它消息,setTimeout 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。

下面的例子演示了这个概念(setTimeout 并不会在计时器到期之后直接执行):

const s = new Date().getSeconds();

setTimeout(function() {
// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}

根据刚才的说法,对于刚才这段代码,setTimeout 的回调至少要等到 while loop 执行结束之后才能执行。所以在 setTimeout 中,并不存在真正的“零延迟”。

setTimeout “零延迟”

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

(function() {
console.log('这是开始');
setTimeout(function cb() {
console.log('这是来自第一个回调的消息');
});
console.log('这是一条消息');
setTimeout(function cb1() {
console.log('这是来自第二个回调的消息');
}, 0);
console.log('这是结束');
})();

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

setTimeout 在循环中的大坑

setTimeout 只要不塞进循环里就还相安无事。下面的例子是大坑中的一个:

for(var i=0; i<9; i++){
setTimeout(function(){
console.log(i);
}, 0);
}

在上方的“零延迟”部分介绍的特性在此处通用,所以所有 setTimeout 的回调会被塞到一起执行。