了解JavaScript中的异步[Sukhjinder Arora的翻译]

哈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运行时的调用堆栈,事件循环,消息队列/任务队列和微任务队列

Source: https://habr.com/ru/post/zh-CN439620/


All Articles