setTimeout或setInterval使用线程来触发吗?

20
我一直在阅读这篇文章http://ejohn.org/blog/how-javascript-timers-work/,学习有关setTimeoutsetInterval和其他异步任务,如按钮单击。我知道JS是单线程的,这意味着回调函数(也称为事件处理程序)将按顺序排队执行。但是,看看下面这张图片,它是我从上面链接的文章中截取的:

enter image description here

每个块表示一些工作,在大约10毫秒左右时,定时器被触发。我知道它的回调函数被放入队列以供稍后执行,但是为什么事件可以在已经执行某些操作时被调用? setTimeout()开始使用单独的线程在内部计算时间并触发其完成事件,这是因为吗?
请注意,我在这里讨论的不是它的回调执行;相反,我正在尝试理解setTimeout如何计算时间并触发其完成。我知道它的回调将在给定的时间参数之前或稍后被调用,但这是因为当运行时发现有任何要执行的内容时,其回调被排队到以后的时间。
类似于这个问题,为什么浏览器可以在用户单击时执行后台循环,同时接受新的点击注册?
如果你说浏览器维护不同的线程来处理不同的事情,那么我们能否称JS在浏览器中是单线程的?

此外,请查看这个问题的答案:https://dev59.com/jHI-5IYBdhLWcg3w0L_x。你的问题为对话增添了有趣的元素。不过,我不确定它是否是重复的,因为问题本身实质上是不同的。 - Thomas
3个回答

34

JavaScript 是单线程的,这意味着两段 JavaScript 代码不会同时执行(除非使用 worker,但每个 worker 也是单线程的)。但当涉及到环境的 API 时,JavaScript 并不是唯一的因素。像 setTimeoutsetIntervaladdEventListener 这些函数是本地实现的(或者至少在单线程 JavaScript 之外),它们的回调是由环境触发的,例如浏览器。正是环境将这些回调放入队列中,以便单线程 JavaScript 引擎执行它们。这就是为什么浏览器能够接受新的点击事件来触发,即使它仍在执行其他 JavaScript 代码的原因。这就是 JavaScript 被认为是事件驱动的原因。


2
很抱歉,Thomas,我手边没有任何链接。但是“本地实现”指的是这些API由主机环境实现,不会在单线程JavaScript中实现或执行,只是回调函数。 - Alexander O'Mara
2
浏览器也不一定是多线程的;你可以非常容易地使用相当标准的事件循环来实现问题中显示的图像,不需要线程。 - Eevee
这并不完全正确。多个代码片段可以同时执行,但它们必须在单独的Web Worker(线程!)上运行。然而,Web Worker必须以单线程方式相互通信和与主线程通信。 - Agamemnus
@Agamemnus 嗯,技术细节。我已经添加了一条注释。 - Alexander O'Mara
非常棒的答案!非常简单易懂且准确。 - Gaspa79
显示剩余3条评论

4

不需要其他线程来解释这种行为。当计时器记录被添加到队列中时,它只是静静地等待。如果事件循环再次获得控制权,则简单地检查是否有计划的任务时间已到。

也不需要额外的线程来保持全局时间,因为操作系统或执行环境已经提供了这个功能。

以这个简单的PhantomJS脚本为例:

function longRunningTask() {
    for(var i = 0; i < 100000; i++) {
        var s = "",
            s2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
        for(var j = 0; j < 1000; j++) {
            s += s2;
        }
    }
}

var start = new Date().getTime();
console.log("begin");
setTimeout(function(){
    console.log("end timer 1s " + (new Date().getTime() - start));
}, 1000);

setTimeout(function(){
    console.log("end timer 10s " + (new Date().getTime() - start));
}, 10000);
longRunningTask();
console.log("end longRunningTask " + (new Date().getTime() - start));

setTimeout(function(){
    console.log("EXIT");
    phantom.exit();
}, 11000);

下面是输出结果:

开始
结束 longRunningTask 5025
结束计时器 1秒 5029
结束计时器 10秒 10001
退出

只有当控制权还给事件循环时,1秒计时器才会触发。


2

目前(Ecmascript 6),您不必担心状态在函数中途发生变化。

但是很多事情确实在后台运行,它们会在自己的隔离环境中进行更改,然后将作为数据块潜在地发送到主环境。XMLHttpRequest回调队列、定时器和Web Workers都这样做。它们都可能同时运行(在多个CPU上),但它们都以顺序方式与主环境通信。


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