文章目录
  1. 1. 引言
  2. 2. 浏览器中的JavaScript
  3. 3. JavaScript 引擎
  4. 4. OK,但是这根Event Loop有毛线关系?
  5. 5. 总结
  6. 6. 译注

本文为翻译文章,原文

引言

如果你像我一样热爱JavaScript。没错,它不是完美的语言,但是这个世界上哪有「完美的语言」这种存在?所以尽管Javascript有这样或那样的缺陷,我仍然喜欢web编程以及JavaScript给予我的编写连接这个世界的应用的能力。

但是JavaScript是有深度的——它有一个复杂的内部机制,你需要花费一定的时间去理解。其中一个有深度的地方就是Event Loop。当然,即便是在对JavaScript的Event Loop没有精确理解的情况下,也能在很长时间内,正常使用javascript进行编程。然而,我希望本篇博客能带你走入Event Loop的世界,让你意识到这玩意儿并不是那么难以理解。

浏览器中的JavaScript

当我们思考JavaScript的时候,我们通常会默认一个语境前提——web浏览器——这使得大多数人都是在客户端编写JavaScript代码。事实上,意识到运行任何web应用实际上包含一些列技术,像JavasCript引擎(如Chrome的V8引擎),一堆Web API(如DOM),还有时间轮询(Event Loop)和事件队列(Event Queue)。

看到上面这些内容之后,你可能会想:『艹,这下看起来更复杂了…』——或许的确是这样——但是你很快就会看到,上面这些技术的核心思想真的没有那么复杂,甚至你可能发现,实现它们会非常容易。

在深入研究 event loop 之前,我们需要对JavaScript引擎和它的工作原理有一些基本的理解。

JavaScript 引擎

目前不同的JavaScript引擎实现有好几种,但是当前最流行的实现版本是Google Chrome的 V8 引擎(这个引擎不受限于浏览器端,在服务器端的Nodejs使用的也是它)。说了这么多,那JavaScript引擎到底是干啥的?其实很简单——它的工作就是遍历Web应用中的每一行JavaScript代码,并且逐个执行(process one at a time)。你没有看错——就是逐个执行,这意味着JavaScript是单线程的。这一特点的主要影响是,如果你执行一行需要非常非常长的时间才能返回结果的代码,这之后的代码都会被阻塞掉。我们当然不想写出这么阻塞的代码——特别是在浏览器中。想象一下你正在查看一个Web网页并且垫底了某个按钮…它就这么耗在了那里。你试着点击其他的按钮,然而并没有什么卵用,啥作用都没起。这种蛋疼的局面只能归罪于点击按钮的时候触发了某些代码的执行(假设没有bug的话),但是这些代码阻塞在了那里。

另一方面,JavaScript引擎如何知道一次只执行一行代码的?它使用了一个调用栈(call stack)。你可以把调用栈想象成一个叠罗汉活动——第一个叠罗汉的只能最后一个下来,对应的是最后一个叠罗汉的第一个下来(原文用乘电梯作比喻,个人认为不是很形象,改成了叠罗汉)。

下面来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Within main.js */
var firstFunction = function () {
console.log("I'm first!");
};
var secondFunction = function () {
firstFunction();
console.log("I'm second!");
};
secondFunction();
/* Results:
* => I'm first!
* => I'm second!
*/

下面是执行代码中,调用栈中发生的一系列事件:

  • Main.js 首先被执行

初始状态

  • secondFunction 被调用

secondFunction 被调用之后

  • 调用secondFunction的时候,内部函数firstFunction也被调用:

firstFunction被调用之后

  • 执行firstFunction的时候,会输出”I’m first!”,并且由于在firstFunction中,没有其他代码要执行,整个firstFunction的执行到此结束,被移出调用栈:

当firstFunction 返回之后

  • firstFunction执行结束返回之后,secondFunction继续执行,输出”I’m second!”。一旦输出完毕之后,secondFunction函数中,也没有其他代码要执行,整个secondFunction函数执行完毕,被移出调用栈:

当secondFunction返回之后

  • 最后,由于main.js中没有其他代码要执行,main.js也被移出调用栈:

当main.js返回之后

OK,但是这根Event Loop有毛线关系?

现在你已经明白JavaScript引擎中的调用栈是如何工作的了,让我们回到之前的代码阻塞的思路上来。当然,你已经意识到应该避免出现这些阻塞,但是该怎么做?幸运的是,JavaScript提供了一种机制,它基于异步回调函数(asynchronous callback function)的方式来实现。这个概念看起来有些吓人,不过不用担心——所谓异步函数和你在JavaScript中使用的普通函数没有什么两样,只不过是加了一些它会在之后执行(而不是立即)执行的手段。如果你使用过Javascript的setTimeout函数,你其实已经使用过异步回调函数的概念了。下面来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Within main.js */
var firstFunction = function () {
console.log("I'm first!");
};
var secondFunction = function () {
setTimeout(firstFunction, 5000);
console.log("I'm second!");
};
secondFunction();
/* Results:
* => I'm second!
* (And 5 seconds later)
* => I'm first!
*/

下面是调用栈中的一系列活动(我们省去了前面的函数加入调用栈的过程,直接来看setTimeout函数):

  • 当secondFunction 被推入调用栈之后,setTimeout 函数被调用,而且也被压入调用栈中:

在setTimeout函数执行之前

  • 当setTimeout函数被执行的时候,发生了一些比较特殊的事——浏览器把setTimeout的回调函数(在本例中是firstFunction)存入一个Event Table中。可以将Event Table想象成一个电话注册本:调用栈会告诉event table注册一些特定的函数,并且在指定事件发生时会调用他们。当这些指定事件发生时,event table仅仅是简单地把要调用的函数移入Event Queue中去。event queue的美妙之处在于它提供了一个简单等待区域,函数在此区域内等待被移入调用栈进行调用。

你或许会问:『究竟什么情况下,event queue中的函数才会被移入调用栈中?』。实际上,JavaScript 遵从一个简单的法则:存在一个监控进程不断检查调用栈是否为空,当调用栈为空的时候,检查事件队列(event queue)中是否有待调用的函数。如果事件队列中存在待调用的函数,队列头部的函数被移入调用栈执行。如果事件队列为空,监控进程就保持轮询状态。
瞧,我刚才描述的内容就是臭名昭著的Event Loop(事件轮询)了!

  • 现在回到我们之前的setTimeout的例子。执行setTimeout函数的过程中,引擎将它的回调函数(在本例中为firstFunction)添加到 event table中,同时注册触发事件为5秒延迟。

当setTimeout函数被执行之后

  • 下面是另一个让你觉得「啊哈,是这样!」的时刻——注意到一旦回调函数被移入event table之后,没有代码在阻塞了!浏览器在执行后面的任何代码之前,并不会在那里傻等5秒了——它直接执行secondFunction函数中setTimeout函数后面的代码,在这里是console.log语句。

secondFunction执行完毕之后

  • 在后台,event table 一直监控是否有指定的事件发生,如果有将触发把对应的函数移入事件队列(event queque)的动作。在上面的例子中,secondFunction到这里已经执行完毕,于此同时main.js到这里也执行完成了。

当main.js执行完成之后

  • 大约算来,在回调函数放入event table后5秒钟,event table会把firstFunction移入事件队列中。

main.js执行结束后约5秒钟

  • 一旦 firstFunction执行完毕之后,我们会回到调用栈为空的状态,这个时候event table,event queue也都为空。

当firstFunction执行完毕之后

总结

我承认我上面的解释掩盖了JavaScript引擎中,以及event table,event queue和event loop中非常多的实际实现细节。但是,对于大部分人来说,我们仅仅需要对JavaScript执行异步函数时发生的事情有一个笼统的认识。我希望上面的解释能帮助你明晰背后的机制,并且满足平常的web开发工作。

译注

问题:setTimeout中设置的时间是准确的吗?

文章目录
  1. 1. 引言
  2. 2. 浏览器中的JavaScript
  3. 3. JavaScript 引擎
  4. 4. OK,但是这根Event Loop有毛线关系?
  5. 5. 总结
  6. 6. 译注