3

有人可以向我解释为什么在运行程序时收到的警报总是“aaa”后跟“bbb”吗?我希望它遵循以下步骤:

  1. 我启动程序

  2. 程序运行并到达 setTimeout 行。它设置计时器,并且该计时器将从该时间点开始在 5 秒内触发(而不是在程序完成时)。事件队列中还没有任何内容

  3. 然后,在 5 秒过去之前,我单击该文档。由于程序很忙并且仍在运行循环,它会将此点击事件的回调放入事件队列中。这是目前偶数队列中唯一的事件(之前设置的计时器尚未触发)

  4. 5 秒过去了,循环可能仍在运行,我们用 setTimeout 设置的计时器触发。这会将计时器回调“aaa()”作为要执行的第二个事件回调放入事件队列,紧随已在队列中的单击事件之后。

  5. 因此,当程序完成时,我希望它首先触发“bbb”,因为首先触发此事件(在 5 秒前单击),然后是“aaa”(setTimeout 事件触发),但我总是得到“aaa”,然后是“bbb” “, 这是为什么?

事件队列不是先进先出(先进先出)吗?它依赖于浏览器吗?为什么会这样?

function aaa() {
    alert("aaa");
}

setTimeout(aaa, 5000);

document.onclick = function () {
    alert("bbb");
}

for (i = 0; i < 1000000; i++) {
    console.log(i);
}

JSFiddle 在这里http://jsfiddle.net/sQvYG/1/

编辑:除非我遗漏了一些东西(可能非常尴尬),否则上面的代码看起来与 John Resigs 博客文章在http://ejohn.org/blog/how-javascript-timers-work的详细解释非常相似/。完全相同的事情,计时器启动,点击发生并添加到队列中。计时器触发并添加到队列中。执行结束并首先执行单击处理程序,这就是我所期望的。任何人都可以解释为什么这对我来说没有以正确的顺序发生?

编辑:感谢 bfavaretto 提供了已接受的答案,并且非常友好地详细解释了队列,我开始明白没有一个,而是放置事件的多个“任务队列”。所以我上面最初描述的 5 个步骤实际上是这些:

STEP 1. (同理)我启动程序

第 2 步。(这是相同的)程序运行并到达 setTimeout 行。它设置计时器,并且该计时器将从该时间点开始在 5 秒内触发(而不是在程序完成时)。事件队列中还没有任何内容。

我也相信此时任务队列中还没有任何东西,正如我在 mozilla 开发页面https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/EventLoop上发现的那样:

“调用 setTimeout 将在作为第二个参数传递的时间之后向队列添加一条消息。”

我还在 Chrome 中验证了这一点,似乎回调在实际触发时被添加到任务队列中。

第 3 步。然后,在 5 秒过去之前,我单击文档。由于程序很忙并且仍在运行循环,因此它将将此单击事件的回调放入浏览器为此类事件提供的任务队列之一(鼠标单击,ui 任务队列)。这是目前此任务队列中的唯一事件。其他任务队列(例如超时)目前是空的。

第 4 步。 5 秒过去了,循环可能仍在运行,我们用setTimeout火设置了计时器。这会将计时器回调“aaa()”放入浏览器专门为此类事件(超时)提供的任务队列中。所以现在我们有一个处理用户交互的任务队列,我们​​有一个回调在等待(第 3 点中我们点击的回调)。我们还有另一个处理超时的任务队列,我们​​还有一个回调在那里等待。这是aaa()5 秒后刚刚触发的方法。

STEP 5. 所以当程序完成时,我们有两个任务队列,每个队列都有一个回调。现在,即使首先将单击放入其任务队列,也不能保证首先处理此任务队列(即用户交互或处理鼠标单击的东西)。这完全取决于浏览器的实现,这让我很头疼,直到 bfavaretto 在他的回答中解释它。

所以在这个例子中我只提到了两个任务队列,但是浏览器可能有其他的。当程序运行并且忙时,每种类型的事件都被放置在其类型的任务队列中。程序完成后,浏览器决定要处理哪些任务队列,从那里抓取回调,执行,一旦完成,从同一个或另一个任务队列中抓取一些东西等等。所以实际上没有办法知道接下来会处理哪个队列。然而,每个特定队列中的任务总是按顺序处理。

Ooff .. 现在这似乎是有道理的,Resig 的文章让我对事情是如何完成的有了总体了解,但事实证明还有更多内容,希望最终我能正确理解它。

4

2 回答 2

4

程序运行并到达 setTimeout 行。它设置计时器,并且该计时器将从该时间点开始在 5 秒内触发(而不是在程序完成时)。事件队列中还没有任何内容

计时器回调立即添加到队列中。如果事件循环在 5000 毫秒过去之前滴答作响,它将被推迟。for这意味着如果循环尚未完成,计时器将花费超过 5 秒的时间。

然后,在 5 秒过去之前,我单击该文档。

这是有缺陷的。如果循环仍在运行,则 UI 被阻止,并且您的点击没有机会在第一个警报(“aaa”)之前注册。

5 秒过去了,循环可能仍在运行,我们用 setTimeout 设置的计时器触发。

也有缺陷。在循环结束之前,我们仍然处于事件循环的第一个滴答声中for。在此之前无法触发计时器回调。

于 2013-09-20T02:45:15.570 回答
3

确切的行为取决于用户代理。在 Chrome 中,您的小提琴首先发出“aaa”警报,但在 Firefox 中,它首先发出“bbb”警报(假设在这两种情况下,当 UI 被for循环阻止时执行鼠标单击,并且计时器在该循环完成之前到期)。

我相信 Resig 的文章是他对 Web 浏览器(或者可能是特定的 Web 浏览器)在撰写时(早在 2008 年)的行为方式的观察的结果。它没有详细介绍某些行为,可能是因为当时没有明确的规范(我不确定)。采取以下段落,这是您问题的关键(强调我的):

在 JavaScript 的初始块完成执行后,浏览器立即提出问题:等待执行的是什么?在这种情况下,鼠标单击处理程序和计时器回调都在等待。 然后浏览器选择一个(鼠标点击回调)并立即执行。计时器将等到下一个可能的时间,以便执行。

这篇文章没有解释为什么首先选择鼠标点击。也许 Resig 表​​示基于 Firefox 的行为方式。另一方面,我的直觉更接近 Chrome 所做的:计时器回调首先添加到队列中,因此它首先触发。Resig 的一些陈述含糊不清,一些不准确;他说计时器和间隔“触发”和“执行”没有定义这些术语的含义(例如:“当鼠标单击处理程序正在执行第一个间隔回调执行时”;“执行”似乎有两种不同的含义,即同步代码的执行,以及定时器的超时)。

编辑
我刚刚检查了Resig 的书。他在书中加入了该文章的修订和扩展版本,以更清晰的方式处理概念和术语。他还引入了一些关于某些行为依赖于浏览器的额外警告。然而,他也引入了一些东西(可能是一个错误),这使得整个事情仍然令人困惑;截至今天(2013-09-20),勘误表中没有任何内容。

感谢您的问题,我现在意识到了这一点;当我第一次阅读 Resig 的文章时,我以为我终于理解了定时器和事件循环的内部工作原理。但是是时候寻找更准确的答案了,所以让我们转向 HTML5 规范,看看它是怎么说的。

我建议你看一下计时器部分,但关键是在事件循环和排队任务部分中找到:

一个事件循环有一个或多个任务队列。任务队列是任务的有序列表,可以是:

事件
在特定的 EventTarget 对象上异步分派 Event 对象是一项任务。

注意:并非所有事件都使用任务队列分派,许多事件在其他任务期间同步分派。

解析
HTML 解析器标记一个或多个字节,然后处理任何生成的标记,通常是一项任务。

回调
异步调用回调是一项任务。

使用资源
当算法获取资源时,如果获取是异步发生的,那么一旦部分或全部资源可用,对资源的处理就是一项任务。

响应DOM 操作
一些元素具有响应 DOM 操作而触发的任务,例如,当该元素插入到文档中时。

当用户代理要对任务进行排队时,它必须将给定任务添加到相关事件循环的任务队列之一。来自一个特定任务源的所有任务(例如,计时器生成的回调、为鼠标移动而调度的事件、为解析器排队的任务)必须始终添加到同一个任务队列中,但来自不同任务源的任务可以放在不同的任务队列。

例如,用户代理可以有一个用于鼠标和按键事件(用户交互任务源)的任务队列,另一个用于其他所有事件。然后,用户代理可以在四分之三的时间内将键盘和鼠标事件优先于其他任务,保持界面响应但不会饿死其他任务队列,并且永远不会无序地处理来自任何一个任务源的事件。

因此,我们有属于许多任务队列(或任务源)之一的任务。定时器回调被添加到一个队列,点击事件被添加到另一个队列。每个队列都是有序的,但用户代理可以自由决定队列之间的优先级。注意最后一段。似乎 Firefox 优先考虑 UI 事件队列。Chrome 似乎优先考虑计时器回调任务所在的不同任务源。

于 2013-09-20T17:36:30.933 回答