事件轮询(Event Loop)

让我们来做一个(也许是令人震惊的)声明:尽管明确地允许异步JS代码(就像我们刚看到的超时),但是实际上,直到最近(ES6)为止,JavaScript本身从来没有任何内建的异步概念。

什么!? 这听起来简直是疯了,对吧?事实上,它是真的。JS引擎本身除了在某个在被要求的时刻执行你程序的一个单独的代码块外,没有做过任何其他的事情。

“被’谁’要求”?这才是重要的部分!

JS引擎没有运行在隔离的区域。它运行在一个 宿主环境 中,对大多数开发者来说这个宿主环境就是浏览器。在过去的几年中(但不特指这几年),JS超越了浏览器的界限进入到了其他环境中,比如服务器,通过Node.js这样的东西。其实,今天JavaScript已经被嵌入到所有种类的设备中,从机器人到电灯泡儿。

所有这些环境的一个共通的“线程”(一个“不那么微妙”的异步玩笑,不管怎样)是,他们都有一种机制:在每次调用JS引擎时,可以 随着时间的推移 执行你的程序的多个代码块儿,这称为“事件轮询(Event Loop)”。

换句话说,JS引擎对 时间 没有天生的感觉,反而是一个任意JS代码段的按需执行环境。是它周围的环境在不停地安排“事件”(JS代码的执行)。

那么,举例来说,当你的JS程序发起一个从服务器取得数据的Ajax请求时,你在一个函数(通常称为回调)中建立好“应答”代码,然后JS引擎就会告诉宿主环境,“嘿,我就要暂时停止执行了,但不管你什么时候完成了这个网络请求,而且你还得到一些数据的话,请 回来调 这个函数。”

然后浏览器就会为网络的应答设置一个监听器,当它有东西要交给你的时候,它会通过将回调函数插入 事件轮询 来安排它的执行。

那么什么是 事件轮询

让我们先通过一些假想代码来对它形成一个概念:

  1. // `eventLoop`是一个像队列一样的数组(先进先出)
  2. var eventLoop = [ ];
  3. var event;
  4. // “永远”执行
  5. while (true) {
  6. // 执行一个"tick"
  7. if (eventLoop.length > 0) {
  8. // 在队列中取得下一个事件
  9. event = eventLoop.shift();
  10. // 现在执行下一个事件
  11. try {
  12. event();
  13. }
  14. catch (err) {
  15. reportError(err);
  16. }
  17. }
  18. }

当然,这只是一个用来展示概念的大幅简化的假想代码。但是对于帮助我们建立更好的理解来说应该够了。

如你所见,有一个通过while循环来表现的持续不断的循环,这个循环的每一次迭代称为一个“tick”。在每一个“tick”中,如果队列中有一个事件在等待,它就会被取出执行。这些事件就是你的函数回调。

很重要并需要注意的是,setTimeout(..)不会将你的回调放在事件轮询队列上。它设置一个定时器;当这个定时器超时的时候,环境才会把你的回调放进事件轮询,这样在某个未来的tick中它将会被取出执行。

如果在那时事件轮询队列中已经有了20个事件会怎么样?你的回调要等待。它会排到队列最后——没有一般的方法可以插队和跳到队列的最前方。这就解释了为什么setTimeout(..)计时器可能不会完美地按照预计时间触发。你得到一个保证(粗略地说):你的回调不会再你指定的时间间隔之前被触发,但是可能会在这个时间间隔之后被触发,具体要看事件队列的状态。

换句话说,你的程序通常被打断成许多小的代码块儿,它们一个接一个地在事件轮询队列中执行。而且从技术上说,其他与你的程序没有直接关系的事件也可以穿插在队列中。

注意: 我们提到了“直到最近”,暗示着ES6改变了事件轮询队列在何处被管理的性质。这主要是一个正式的技术规范,ES6现在明确地指出了事件轮询应当如何工作,这意味着它技术上属于JS引擎应当关心的范畴内,而不仅仅是 宿主环境。这么做的一个主要原因是为了引入ES6的Promises(我们将在第三章讨论),因为人们需要有能力对事件轮询队列的排队操作进行直接,细粒度的控制(参见“协作”一节中关于setTimeout(..0)的讨论)。