【译】单线程的 JavaScript 是如何做到异步的呢?

275 阅读3分钟

翻译自:If Javascript Is Single Threaded, How Is It Asynchronous?

JavaScript 是一门单线程的语言,这意味着它只有一个调用栈(stack)和一个内存堆(Heap)。如我们所料,它按序执行代码,在执行下个代码片段之前,必须先执行完当前的代码片段。这个过程是同步的(synchronous),但有时它却是有害的。例如,一个函数的执行很耗时或者这个函数必须等待某个事物,这时整个程序就都被冻结了。

能够说明这个情况的一个很好的例子就是 window.alert 函数:alert("Hello World")

这个时候你不能和 web 页面进行任何的交互,除非你点击确定按钮释放了这个弹窗。

那么,我们如何使用 Javascript 编写异步代码呢?

为此,我们要感谢 JavaScript 引擎(V8、Spidermonkey、JavaScriptCore 等等),它们提供了在后台处理这些任务(译者注:指的是诸如上述耗时、需要等待的任务等)的 Web APIs。调用栈识别 Web API 的函数,并把它们移交给浏览器处理。一旦这些任务被浏览器处理完毕,它们就会被返回,然后以回调的形式被压入栈中执行。(译者注:其实,这里作者省略了有关回调队列的描述。还有一点需要注意的就是,往回调队列添加回调是在浏览器完成了并行的后台任务(包括事件监听)之后才做的事。)

打开你的浏览器控制台,输入 window 后回车,你会看到 Web API 必须提供的大部分内容,这包括 AJAX 请求,事件监听者,fetch API 和 setTimeout。JavaScript 使用低级编程语言,像 C++ 在后台执行这些操作。

译者注:对 setTimeout 来说,后台任务就是“计时”这一耗费时间的任务。有人可能会觉得这是一句废话,计时肯定耗费时间啊,其实我们可以把“计时”抽象成一个耗时任务的特例,只不过我们事先知道了这个耗时任务所耗费的时间。

让我们看一个简单的示例,在控制台上运行以下代码:

console.log("first")
setTimeout(() => {
    console.log("second")
}, 1000)
console.log("third")

我们得到了什么?

first
third
undefined
second

感觉很怪,对吧?好吧,那就让我们逐行分析下代码:

console.log("first") 第一个运行在调用栈上,所以它的输出第一个被打印出来。接着,引擎注意到了 setTimeout,它不是由 JavaScript 来处理的,引擎把它推给 Web API 来并行(译者注:原文是异步,译者认为这样的说法不正确)完成(译者注:Again, 只是计时)。调用栈继续前行,而不再关心移交给 Web API 的代码,那么 console.log("third") 就被执行了。

(译者注:此时,该脚本生命周期内的同步代码已经执行完毕)接下来,JavaScript 引擎的事件循环(Event Loop)参与了进来,像一个旅途中小孩子一样问道:“我们到了吗?”事件循环等待着事件的到来。计时结束,回调被调用,最后输出 second

译者注:译者没有理解原文的意思,上一段并没有按照原文翻译。第 3 个输出 undefinedconsole.log("third") 的返回值,控制台下运行代码会返回最后一行代码的返回值。

这有一个很不错的网站,以慢速动画的形式演示上述发生的一切:

latentflip.com/loupe

我建议你在这个沙箱中多多尝试以帮助巩固你的理解。它帮助我了解到了异步代码是如何与单线程的 Javascript 一起工作的。