哈Ha! 我向您介绍Sukhjinder Arora撰写的文章
“ Understanding Asynchronous JavaScript” 。
翻译作者的译文:我希望本文的翻译可以帮助您熟悉一些新的有用的知识。 如果文章对您有所帮助,请不要偷懒,并感谢原始作者。 我并不假装自己是专业的翻译人员,我只是开始翻译文章,并且很高兴收到任何有意义的反馈。JavaScript是一种单线程编程语言,一次只能执行一件事。 也就是说,在一个线程中,JavaScript引擎一次只能处理1条语句。
尽管单线程语言使您不必担心并发问题,从而使编写代码变得更加容易,但这也意味着您将无法执行长时间的操作(例如访问网络)而不会阻塞主线程。
提交一些数据的API请求。 根据情况,服务器可能需要一些时间来处理您的请求,而主流的执行将被阻止,因为您的网页将停止响应该请求。
这就是JavaScript异步发挥作用的地方。 使用JavaScript异步(回调,承诺和异步/等待),您可以执行长网络请求,而不会阻塞主线程。
虽然不必成为一名优秀的JavaScript开发人员就必须学习所有这些概念,但了解它们很有用。
因此,事不宜迟,让我们开始吧。
同步JavaScript如何运作?
在开始异步JavaScript的工作之前,我们首先了解一下同步代码如何在JavaScript引擎中运行。 例如:
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
为了了解如何在JavaScript引擎内部执行上述代码,我们需要了解执行上下文和调用堆栈(也称为执行堆栈)的概念。
执行上下文
执行上下文是在其中评估和执行代码的环境的抽象概念。 每当在JavaScript中执行任何代码时,它都会在执行上下文中运行。
功能代码在函数执行的上下文中执行,而全局代码又在全局执行上下文中执行。 每个函数都有其自己的执行上下文。
调用堆栈
调用堆栈是具有LIFO结构(后进先出,先使用)的堆栈,该堆栈用于存储在代码执行期间创建的所有执行上下文。
JavaScript只有一个调用堆栈,因为它是一种单线程编程语言。 LIFO结构意味着只能从堆栈顶部添加和删除元素。
现在让我们回到上面的代码片段,尝试了解JavaScript引擎如何执行它。
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();

那么这里发生了什么?
当代码开始执行时,将创建一个全局执行上下文(表示为
main() )并将其添加到调用堆栈的顶部。 当遇到对
first()函数的调用时,它也会被添加到堆栈的顶部。
接下来,将
console.log(“嗨,那里!”)放在调用堆栈的顶部,执行后将其从堆栈中删除。 之后,我们调用
第二个()函数,因此将其放置在堆栈的顶部。
console.log('Hello there!')被添加到堆栈的顶部,并在执行完成后从堆栈中删除。
第二个()函数完成,它也从堆栈中删除。
console.log('The End')被添加到堆栈的顶部,并在末尾被删除。 之后,
first()函数终止,并且也从堆栈中删除。
程序执行结束,因此将全局调用上下文(
main() )从堆栈中删除。
异步JavaScript如何工作?
现在,我们对调用堆栈以及同步JavaScript的工作方式有了基本的了解,让我们回到异步JavaScript。
有什么阻碍?
假设我们正在同步处理图像处理或网络请求。 例如:
const processImage = (image) => { console.log('Image processed'); } const networkRequest = (url) => { return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting();
图像处理和网络请求需要时间。 调用
processImage()函数时,其执行将花费一些时间,具体取决于图像的大小。
当
processImage()函数完成时,会将其从堆栈中删除。 之后,将调用
networkRequest()函数并将其添加到堆栈中。 在完成执行之前,这将再次花费一些时间。
最后,当执行
networkRequest()函数时,由于仅包含
console.log方法,并且调用了
greeting()函数,并且该方法通常运行很快,
greeting()函数将立即执行并立即结束。
如您所见,我们需要等待函数(例如
processImage()或
networkRequest() )完成。 这意味着这些函数会阻塞调用堆栈或主线程。 结果,在执行上面的代码之前,我们无法执行其他操作。
那么解决方案是什么?
最简单的解决方案是异步回调函数。 我们使用它们使我们的代码成为非阻塞的。 例如:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest();
在这里,我使用了
setTimeout方法来模拟网络请求。 请记住,
setTimeout不是JavaScript引擎的一部分,而是所谓的Web API(在浏览器中)和C / C ++ API(在node.js中)的一部分。
为了了解如何执行此代码,我们需要处理一些其他概念,例如事件循环和回调队列(也称为任务队列或消息队列)。

事件循环,Web API和消息队列/任务队列不是JavaScript引擎的一部分;它们是Node.js中JavaScript JavaScript运行时或JavaScript运行时的一部分(对于Nodejs)。 在Nodejs中,Web API被C / C ++ API取代。
现在,让我们回到上面的代码,看看在异步执行的情况下会发生什么。
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End');

将上述代码加载到浏览器中后,
console.log('Hello World')将添加到堆栈中,并在执行完成后从堆栈中删除。 接下来,
遇到对
networkRequest()函数的调用;该调用被添加到堆栈的顶部。
接下来,调用
setTimeout()函数并将其放置在堆栈的顶部。
setTimeout()函数具有2个参数:1)回调函数和2)时间(以毫秒为单位)。
setTimeout()在Web API环境中启动计时器2秒钟。 至此,
setTimeout()完成并从堆栈中删除。 之后,
console.log('The End')被添加到堆栈,执行并在完成后从堆栈中删除。
同时,计时器已过期,现在回调已添加到消息队列中。 但是回调不能立即执行,并且事件处理周期才进入该过程。
事件循环
事件循环的任务是跟踪调用堆栈并确定其是否为空。 如果调用堆栈为空,则事件循环在消息队列中查找以查看是否有等待完成的回调。
在我们的例子中,消息队列包含一个回调,执行堆栈为空。 因此,事件循环将回调添加到堆栈的顶部。
将
console.log(“异步代码”)添加到堆栈的顶部后,执行并从中删除。 至此,回调已完成并从堆栈中删除,程序已完全完成。
DOM事件
消息队列还包含来自DOM事件的回调,例如单击和键盘事件。 例如:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); });
对于DOM事件,事件处理程序被Web API包围,等待特定事件(在这种情况下为单击),并且在发生此事件时,将回调函数放在消息队列中,等待其执行。
我们了解了如何执行异步回调和DOM事件,它们使用消息队列存储等待执行的回调。
ES6 MicroTask队列
注意事项 翻译作者:在本文中,作者使用了消息/任务队列和作业/微型任务队列,但是如果您翻译任务队列和作业队列,那么从理论上讲,结果是一样的。 我与译文的作者进行了交谈,并决定仅省略作业队列的概念。 如果您对此有任何想法,我会在评论中等待您
链接到同一作者的诺言翻译
ES6引入了微任务队列的概念,Promises在JavaScript中使用了微任务队列。 消息队列和微任务队列之间的区别在于,微任务队列的优先级高于消息队列,这意味着微任务队列中的“承诺”将比消息队列中的回调更早地执行。
例如:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End');
结论:
Script start Script End Promise resolved setTimeout
如您所见,“ promise”是在
setTimeout之前执行的,因为“ promise”的响应存储在microstask队列中,该队列的优先级高于消息队列。
让我们看下面的示例,这次是2个“ promises”和2个
setTimeout :
console.log('Script start'); setTimeout(() => { console.log('setTimeout 1'); }, 0); setTimeout(() => { console.log('setTimeout 2'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End');
结论:
Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2
同样,我们的两个“承诺”都在
setTimeout内部的回调之前执行,因为事件处理循环认为微任务队列中的任务比消息队列/任务队列中的任务更重要。
如果在任务执行期间从微任务队列中出现另一个“ Promise”,它将被添加到此队列的末尾,并在从消息队列进行回调之前执行,无论它们等待执行多长时间。
例如:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve('Promise 3 resolved'); }) }).then(res => console.log(res)); console.log('Script End');
结论:
Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout
因此,微任务队列中的所有任务将在消息队列中的任务之前完成。 也就是说,事件处理循环将首先清除微任务队列,然后才开始从消息队列执行回调。
结论
因此,我们了解了异步JavaScript的工作原理和概念:构成JavaScript运行时的调用堆栈,事件循环,消息队列/任务队列和微任务队列