setInterval和setTimeout是如何工作的?

38

我处于一个尴尬的境地

我已经使用纯JavaScript工作了将近3年,我知道JavaScript是单线程语言, 并且可以通过使用setIntervalsetTimeout函数来模拟异步执行。

但是当我思考它们如何工作时,我不能清楚地理解它们。那么这些函数会如何影响执行上下文呢?

我认为在特定时间只运行代码的一部分,然后切换到另一部分。如果是这样的话,那么大量的setIntervalsetTimeout调用会影响性能吗?


当然。如果你使用 while(true);,那么超时和间隔永远不会运行。 - Jon
你放置的每一行代码都会影响性能,特别是间隔和超时,因为这会让浏览器不断地处理以减少一个变量。因此,如果你有很多这样的代码,你的页面将运行得非常缓慢。 - Fibonacci
3
它们仅在其他所有代码执行完毕且线程空闲时才推迟执行,它们不会创建新线程。 - adeneo
2
想象一条按执行时间排序的任务队列。如果队列顶部的项目的执行时间小于当前时间,则它们将被弹出/执行。如果线程在队列检查之前被阻塞,则队列中的任何任务都不会被执行。 - crush
5个回答

51

Javascript是单线程的,但浏览器不是。浏览器至少有三个线程:Javascript引擎线程、UI线程和计时线程,其中setTimeoutsetInterval的计时是由计时线程完成的。

调用setTimeoutsetInterval时,浏览器中的计时器线程开始倒计时,并在时间到达后将回调函数放入Javascript线程的执行栈中。如果在时间到达时正在执行其他耗时的函数,则setTimeout的回调函数将无法及时完成。


1
虽然浏览器有几个线程,但setInterval是在与脚本相同的线程上调度的。 - Iván Rodríguez Torres
回调函数在堆栈中位于其上方的其他函数完成之前不会被执行。我猜这不是真的。请检查fiddle https://jsfiddle.net/er_markar/ycjc77xa/ - Sunil Garg
1
让我困惑的是,定时器线程将操作放入执行队列中,这不安全,因为它不在管理队列的线程上。除非定时器线程可以暂停事件线程,否则这会导致队列处于不一致状态。 - Dmytro
7
@SunilGarg,你的代码有误。正确的写法应该是setTimeout(alert, 3000),这样才能按照你的预期工作。如果写成setTimeout(alert(), 3000),会先执行alert()函数,然后将其结果传递给setTimeout函数。 - pato
1
定时线程具体是做什么的?它是一个循环,检查系统时间吗? - Ini
显示剩余2条评论

18

setTimeout / setInterval 在 JavaScript 中的工作原理

浏览器有一个定时器函数的 API,就像事件处理函数一样。

'click'

'scroll'

假设您在应用程序中有以下代码:

function listener(){
    ...
}

setTimeout(listener, 300)

function foo(){
    for(var i = 0; i < 10000; i++){
        console.log(i)
    }
}

foo()

![查看JavaScript中函数执行的工作原理][1] [1]: https://istack.dev59.com/j6M6b.webp


根据我们以上编写的代码,此时我们的调用堆栈(Call Stack)将如下所示

调用堆栈 -> foo

假设foo需要1秒才能完成它的执行,由于我们在代码中定义了一个1秒的timeout,并在“foo”完成执行之前即在300ms运行它

那么会发生什么呢?

JavaScript会停止执行foo并开始执行setTimeout吗?

不是这样的

正如我们已经知道的那样,JavaScript是单线程的,因此必须在继续之前完成foo的执行,但是浏览器如何确保在foo执行完毕后“setTimeout”将执行呢?

这就是JavaScript魔法的作用

当300毫秒过去时,浏览器的“Timer API”启动并将超时处理程序放入“消息队列(Message Queue)”中。

此时上面的图片中的“消息队列”将如下所示

消息队列 -> setTimout:listner

然后

调用堆栈 -> foo

当“调用堆栈”变为空即foo完成执行时,上图中的“Event Loop”将从消息队列中获取消息并将其推送到堆栈中

“Event Loop”的唯一工作是在“调用堆栈”变为空且“消息队列”中有条目时,将消息从“消息队列”中出列并将其推送到“调用堆栈”中

此时上面的图片中的消息队列将如下所示

消息队列 ->

然后

调用堆栈 -> listener

这就是setTimeout和setInterval的工作原理,即使我们在setTimeout中指定了300ms,它也会在foo完成执行后执行,即在此案例中1秒后执行。

这就是为什么在setTimeout/setInterval中指定的计时器表示函数执行的最小时间延迟。


在网页中进行 settimeout 调用时,如果没有其他操作,存在哪些开销并不清楚。事件循环是否运行或等待事件?计时线程是否在循环中运行?是否存在任何 CPU 负载? - David Spector

7

Javascript是单线程的,但浏览器不是。

有1个栈用于执行函数和语句。 有1个队列用于排队等待执行的函数。 还有Web API可以在特定时间内保持函数,这些时间定义在setTimeout和setInterval的事件表中。

当Javascript引擎逐行执行Js文件时,如果它找到一行声明或函数调用,它会将其加载到堆栈中并执行。但如果它是setTimeout或setInterval调用,则TIME API(浏览器的Web API之一)会取出与setTimeout或setInterval关联的函数处理程序并保留它。直到规定时间结束。

一旦时间结束,Time Api将该函数放在执行队列的末尾。

现在,该函数的执行取决于队列中前面的其他函数调用。

注意:这个函数调用是在window对象上调用的。

setTimeout(function () {console.log(this)}, 300)

Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}


2

JavaScript是一种单线程的脚本语言,因此它每次只能执行一段代码(由于其单线程的本质),这些代码块中的每一个都会“阻塞”其他异步事件的进程。这意味着当异步事件发生时(例如鼠标点击、定时器触发或XMLHttpRequest完成),它会被排队等待以便稍后执行。

setTimeout() 当你使用setTimeout()时,它只有在队列中轮到它的时候才会执行,如果先前的事件(setTimeout事件)由于某种原因而阻塞,则setTimeout可能会延迟超过setTimeout()函数中指定的时间。在执行setTimeout回调函数期间,如果出现任何事件(例如单击事件),它会被排队等待稍后执行。

setTimeout(function(){
  /* Some long block of code... */
  setTimeout(arguments.callee, 10);
}, 10);

setInterval(function(){
  /* Some long block of code... */
}, 10);

setInterval()

  • setInterval()与setTimeout类似,但会在每次调用函数时(每隔一定时间)继续执行,直到被取消。

  • setTimeout代码执行前,至少会有10ms的延迟(可能会更长,但不会更短),而setInterval会尝试在每10ms执行一次回调函数,无论上一个回调函数何时执行。

  • 如果计时器被阻止立即执行,它将被延迟到下一个可能的执行点(这将比所需的延迟更长)。如果间隔函数需要执行的时间足够长(大于指定的延迟时间),则它们可以连续执行而没有延迟。


1

关于我的经验,如果你要使用setTimeout(),该函数总是会被延迟执行(我指的是稍后执行),或者在未被“延时”的其他代码之后执行。即使是delay=0的函数也可能会出现这种情况,这是由事件循环引起的。

下面是示例:

setTimeout(function() {
    console.log('Second');
}, 0)

console.log('First');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接