说明: 本文来自Medium,作者:Anoop Raveendran

文章版权属于原网站/原作者。我依旧只是个搬运工+不称职的翻译。

“JavaScript 的异步和单线程是怎样实现的?”简单来说,JavaScript 是单线程的但异步并不是在 JavaScript 中实现的,而是基于浏览器中 JavaScript 内核之上实现的并且通过浏览器 APIs 进行访问。

现在我试着通过两个简单的片段详细解释事件循环。

基础结构

浏览器主要组件概述

  • 堆(Heap)—— 对象存储在堆中,堆表示了内存中一个大的非结构化的区域。
  • 栈(Stack)—— 这个表示了 JavaScript 代码执行时的单线程。函数的调用组成了栈的帧(详情见下面的描述)。
  • 浏览器或是 Web APIs 是在浏览器中实现的,能从浏览器和电脑系统得到数据并且在此基础上执行复杂的事情。这些并不属于 JavaScript 语言本身,而是基于浏览器中 JavaScript 内核之上实现的,为你在使用 JavaScript 代码提供了额外的超能力。 举个栗子🌰,地理位置定位 API 提供了一些简单的 JavaScript 方法来检索位置数据,例如在 Google Map 中标记你的位置。在其背后的实现,浏览器其实是调用了一些复杂的底层代码(比如 C++)和设备的 GPS 硬件(或是任何可以定义位置信息)来获取位置信息,返回到浏览器中并在你的 JavaScript 代码中使用。对开发者来言,这些 API 抽象了复杂度。

片段1:详细解释

现在我们有一个主函数包含了在两个 console.log 语句分别在控制台输出 “A” 和 “C”。在两者之间,有一个等待 0 毫秒的 setTimeOut 打印 “B” 的函数。

执行过程的内部实现

  1. 首先是调用主函数(作为帧)加入到栈中。随后浏览器把 console.log(‘A’) 作为第一条语句加入到栈。这条语句执行完成并且此帧弹出。于是在控制台展示了字母 “A”。
  2. 下一条语句( 回调为 exec() 且等待0毫秒,执行的setTimeout() ),推入了执行栈(call stack)并开始执行。setTimeout 函数调用的浏览器的 APIs 用于等待执行回调函数。这一帧(使用 setTimeOut)弹出并且完成交由浏览器。
  3. 当浏览器的定时器(timer)执行回调方法 exec() 时,console.log(‘C’) 加入到栈中。在特定的时候,正如延迟 0 毫秒的时候,(理想情况下)一旦浏览器收到回调,这个回调函数加入到消息队列(message queue)。
  4. 当执行完成主函数的最后一个语句时,这个 main() 函数从执行栈弹出,因此执行栈为空。浏览器从队列向执行栈传递数据时,必须执行栈为空作为前提。这就是为什么就算 setTimeout() 的等待时间为 0 毫秒,它的回调 exec() 必须等到执行栈的所有帧完成。
  5. 现在回调函数 exec() 调入到执行栈并开始执行。控制台显示字母 “C”。这就是 JavaScript 的事件循环。

所以在 setTimeout(函数,延迟时间)中的延迟参数,并不是函数延迟执行的准确时间。它代表在此之后某个时间点执行功能的最短等待时间。

片段2:深入理解

深入解释事件循环

  • runWhileLoopForNSeconds() 函数表示的和名字一致。其会不停的检查经过的时间是否等于此函数传入的第二个参数。重要的是记住 while 循环(与其他语言类似)是一个阻塞语句,意味着,它的执行是发生在执行栈中并且不会使用浏览器 APIs。所以它会阻塞后续的语句,直到完成执行。
  • 所以就上方的代码,尽管 setTimeout 只需等待 0 毫秒并且 while 循环执行 3 秒, 回调函数 exec() 语句卡在消息队列 3 秒。直到 3 秒以后 while 循环一直在执行栈中运行(单线程)。在执行栈为空之后,回调函数 exec() 才移动到执行栈并开始执行。
  • 所以 setTimeout() 的等待参数并不保证一定等于执行开始到结束的时间。它是等待部分的最短时间。

参考资料:

  1. 详解JavaScript中的Event Loop(事件循环)机制
  2. JS Event Loop(需翻墙,强烈推荐 12:50~17:20 之间的内容)
  3. JavaScript 事件循环可视化(”JS Event Loop” 演讲示例)