JavaScript:异步编程的同步解释。






4.95/5 (16投票s)
一个关于运行时实际发生情况的枯燥演讲……
如果你曾经问过 JavaScript 的定义……你会得到这样的答案
JavaScript 是一种单线程、非阻塞、异步、并发语言!
真的吗!……太棒了!
并发、异步、线程、并行编程以及许多这类术语……它们是一团糟。尽管你可以轻松使用其中一个术语来解释你的想法,但也很容易在正确的地方迷失使用正确的术语,即使是经验丰富的开发者也可能在选择正确的词语时不够准确。我在这里试图做的是更深入地理解这些术语之间的区别……重点是 JavaScript 的异步编程。
在异步之前……首先什么是同步代码?!!
如果你用任何流行的编程语言(如 C、Java、C#、Python)写过任何(Hello World)程序,这就是同步代码,代码按编写的顺序逐行执行。
code
Console.log('First Line');
Console.log('Second Line');
Console.log('Third Line');
输出
First Line Second Line Third Line
是的,正如预期的那样,你得到了这个结果……这就是同步代码。
但在继续异步代码之前,让我们更详细地了解一下 JavaScript 运行时对这些代码的实际处理。
JavaScript 运行时引擎
从代码执行的角度来看,我们关注的是任何 JavaScript 引擎(例如 V8,Google 的开源 JavaScript 引擎,用于 Chrome 和 Chromium 项目)的一个重要组件——调用栈。简单来说,调用栈是一种数据结构,V8 在其中存储有关当前正在执行的函数的信息,以跟踪代码的执行流程。
让我们分析一下这样的代码
function sayHello(name){
console.log('Hello, ' + name);
}
function greeting(name){
return sayHello(name);
}
greeting ('Adam');
- 最初,JavaScript 运行时会将一个堆栈帧(比如
main
)推入栈顶,它代表脚本本身或当前代码块。 - 声明
sayHello
函数,这里什么都没有真正执行。 - 声明
greeting
函数。 - 通过运行第 9 行,JavaScript 运行时将
greeting
的地址推入栈顶执行。 - 在
greeting
内部,我们调用了sayHello
函数,这意味着 JavaScript 必须将sayHello
的地址也推入调用栈。 - 再次,在
sayHello
函数内部,JavaScript 运行时发现了对console.log
函数的调用,因此它也将它推入调用栈执行。 - 在
console.log
函数返回后,JavaScript 运行时会弹出console.log
的堆栈帧,并返回到调用栈中的上一个地址,即sayHello
。 - JavaScript 运行时完成
sayHello
函数的执行,并将其堆栈帧从调用栈中弹出。 - 弹出
greeting
的堆栈帧。 - 弹出 main 的堆栈帧。
那么,异步代码呢?为什么?
通过像前面例子一样简单的几行代码,我们无法发现执行此代码的任何问题,程序将顺利运行并完成其工作,但如果我们有一些慢/阻塞的代码行呢?
诸如访问文件、数据库或从网络读取等操作被认为非常慢,并且由于 JavaScript 在浏览器中运行的特性,慢速操作(例如 HTTP 请求)可能会冻结您的网页,因此被认为是阻塞代码。
这就是异步部分。因为 JavaScript 在浏览器中运行,所以它不能执行任何阻塞代码,它不应该等待并冻结网页,否则用户在浏览简单网页时也会获得糟糕的体验。因此,JavaScript 在设计上是非阻塞的,这意味着它不会冻结执行并等待阻塞的代码行,而是使用事件和异步回调技术。
事件和异步回调
在这种方法中,异步回调提供了一种避免阻塞代码的解决方案。回调是一个函数,当事件发生时,它会被注册以供执行。JavaScript 不会等待阻塞代码完成,而是继续执行下一行,并在事件触发时返回到回调。
一个伪回调会看起来像
httpRequest('http://www.google.com/',function myCallBack(){
console.log('Hey!!');
});
这意味着在完成 httpRequest
(阻塞代码)后,将执行 myCallBack
函数(回调)。
简单吧……好的?!
好的,但现在我可以看到 JavaScript 可以同时做多件事情了,它可以执行主线程代码,也可以同时处理事件。这难道不是一种并发或类似的东西吗?!你不是说 JavaScript 是单线程的,而且它一次只能做一件事吗?!!
深入一步……
好吧,在回答这个问题之前,是时候再次深入了解 JavaScript 环境了。请允许我向您介绍新朋友……欢迎事件循环、消息队列和WebAPI。
如果我们想象 JavaScript 运行时环境,它会是这样的
-WebAPI:它是浏览器负责外部 API、DOM API 以及大多数阻塞 I/O 代码(如访问文件、网络请求或计时器(setTimeout
))的部分。它的工作是在浏览器触发特定事件时,取回注册的事件回调。
-消息队列:它是一个简单的队列(FIFO),来自WebAPI的回调会按顺序进入并存储在此。
-事件循环:它的工作是监视调用栈,如果为空,则将消息队列中的下一个任务推入调用栈以供执行。
这可能对您来说是新知识,像访问文件或网络这样的阻塞操作并不是 JavaScript 运行时(V8)本身的一部分,即使是 setTimeout
,它也来自托管环境,即 Web 浏览器(或 NodeJS),浏览器比 JavaScript 运行时引擎的功能要多得多。
对于这样的代码
console.log('first');
setTimeout(function foo(){
console.log('second');
}, 1000);
console.log('third');
输出
first
third
second
当我们将函数 foo
注册为在 1000 毫秒后运行,就会发生这种情况。因此,运行时会运行第一行,然后是第三行,1000 毫秒后,它会返回执行 setTimeout
回调,这就是在调用栈和消息队列中正在发生的事情。
在 setTimeout 的情况下,WebAPI 设置一个 1000 毫秒的计时器,并等待这段时间被触发。一旦被触发,WebAPI 会将计时器的回调函数 foo
排队到消息队列中。
消息队列是事件的回调按顺序存储并准备执行的地方。这里就轮到事件循环了。
事件循环不断监视调用栈,当调用栈为空时,事件循环会将消息队列中的顶部任务推入调用栈以供执行。
我们可以通过以下方式模拟此过程
- 初始堆栈帧,用于开始执行代码。
- JavaScript 运行时将
console.log('first')
推入调用栈执行,我们在输出中得到“first”。 - 调用
setTimeout
设置一个带有回调函数 foo 和 3000 毫秒间隔的计时器。 - WebAPI 创建此计时器并等待 3000 毫秒。
- JavaScript 继续运行代码并弹出
setTimeout
的堆栈帧。 - JavaScript 进入下一行,并将
console.log("third")
推入调用栈,我们在输出中得到“third”。 - JavaScript 弹出顶部堆栈帧。
- JavaScript 弹出 main 堆栈帧。
- 3000 毫秒后,计时器事件触发。
- WebAPI 将计时器回调
foo
排队到消息队列中,以便准备执行。 - 事件循环检查调用栈,如果为空,则将
foo
函数推入调用栈。 - 执行 foo 函数的主体,
console.log('second').
- 从调用栈中弹出
console.log('second')
。 - 弹出 foo 的堆栈帧。
这就是 JavaScript 如何通过事件循环和异步回调,在单线程非阻塞设计中处理并发的方式……
请注意
事件循环仅在调用栈为空时才将新任务推入调用栈,设置 setTimeout(function(){}, 0)
(即零延迟情况)并不意味着它会立即触发,它会在它是消息队列中的下一个任务并且调用栈为空时执行,同样 setTimeout(function(){}, 1000)
并不保证在正好 1000 毫秒后执行,但它肯定保证在此之前不会执行。
结论
是的,JavaScript 本身是单线程的,它一次只能做一件事,而且它在设计上是非阻塞的,它通过事件循环和异步回调,在浏览器或托管环境的帮助下处理并发。
参考资料
- https://mdn.org.cn/en-US/docs/Glossary/Call_stack
- https://en.wikipedia.org/wiki/Call_stack
- https://mdn.org.cn/en-US/docs/Web/JavaScript/Event Loop
- https://mdn.org.cn/en-US/docs/WebAPI
- https://www.youtube.com/watch?v=8aGhZQkoFbQ
- http://stackoverflow.com/questions/36233028/how-does-javascript-work
- https://javascript.js.cn/tutorial/settimeout-setinterval
- http://altitudelabs.com/blog/what-is-the-javascript-event-loop/