[译]理解异步JavaScript-事件循环(Understanding Asynchronous JavaScript — the Event Loop)

in #cn6 years ago

原文来自Understanding Asynchronous JavaScript — the Event Loop

JavaScript是一种单线程编程语言,这意味着一次只能发生一件事。 也就是说,JavaScript引擎只能在单个线程中一次处理一个语句。

虽然单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络访问等长时间操作。

想象一下从一个API请求一些数据。 根据不同的情况,服务器可能需要一些时间来处理请求,同时会阻塞了主线程使网页无响应。

这就是异步JavaScript发挥作用的地方。 使用异步JavaScript(例如callbacks、promises或者async/await),你可以执行长网络请求而不会阻塞主线程。

你不是必须得知道在底层JavaScript是如何工作的,但了解它如何工作是有帮助的。

所以不用多说了,让我们开始吧。

同步JavaScript如何工作?

在我们进入异步JavaScript之前,让我们先来了解一下在JavaScript引擎中同步JavaScript代码是如何执行的。例如:

为了了解上述代码在JavaScript引擎中是如何执行的,我们需要知道执行上下文和调用栈的概念。

执行上下文

执行上下文是一个评估和执行JavaScript代码的环境的抽象概念。 每当在JavaScript中运行任何代码时,它都在执行上下文中运行。

函数代码在函数执行上下文中执行,全局代码在全局执行上下文中执行。 每个函数都有它自己的执行上下文。

调用栈

顾名思义,调用栈是一个具有LIFO(后进先出)结构的堆栈,用于存储代码执行期间创建的所有执行上下文。

因为JavaScript是一种单线程编程语言,所以它有一个单独的调用栈。 调用栈具有LIFO结构,这意味着只能从堆栈顶部添加或移除项目。

让我们回到上面的代码片段,尝试理解代码在JavaScript引擎中的执行方式。

这里发生了什么?

当这代码被执行时,一个全局的执行上下文被创建了(比如称呼为main( )),然后被推到了调用栈的顶部。当调用first( )时,它会被推到栈的顶部。

接下来,console.log('Hi there')会被推到栈的顶部,当它结束后,它会被从栈中移除。之后,我们调用了second( ),所以second( )被推到了栈的顶部。

console.log('Hello there')被推到栈的顶部然后结束后被移除。second( )函数执行完毕,所以它从栈中被移除。

console.log('The end')被推到栈的顶部然后结束后被移除。最后,first( )函数执行完毕,所以它被从栈中移除。

这个程序在此刻已经执行完毕,所以它的全局执行上下文(比如称呼为main( ))从调用栈中被移除。

异步JavaScript是如何工作的?

现在我们对调用栈有了一个基本认识,并且知道了同步代码是如何工作的,让我们回到异步JavaScript中来。

什么是阻塞?

假设我们以同步的方式进行图像处理或网络请求,例子如下:

执行图像处理和网络请求需要花些时间。所以当processImage( )函数被调用时,花费的时间取决于图像的大小。

当processImage( )函数执行完毕,它被从栈中移除,networkRequest( )函数被调用并且推到栈中。当然这也需要花费一些时间。

最后当networkRequest( )函数执行完毕。greeting( )函数被调用,因为它就只有一个console.log语句并且console.log语句普遍运行的快,所以greeting( )函数几乎是立刻就执行并且返回了。

所以你看,我们需要去等待函数执行完毕,这意味着这些函数阻塞了调用栈或主线程,所以我们在上述代码仍在执行且没有结果时不能执行其他的任何操作。

所以有什么解决办法?

最简单的解决办法就是异步回调。我们使用异步回调来使我们的代码无阻塞。例如:

这里我使用了setTimeout方法来模拟网络请求。请记住,它不是JavaScript引擎的一部分,它是web APIs(在浏览器中)和C/C++ APIs(在node.js中)的一部分。

为了去了解这里的代码是如何执行的,我们先要知道一些事件循环和调用队列(消息队列)方面的概念。

事件循环、web APIs 和消息队列不是JavaScript引擎的一部分,它们是浏览器端的JavaScript运行环境或者Nodejs端的JavaScript运行环境的一部分。在Nodejs中,web APIs被C/C++ APIs替代。

现在让我们回到之前代码,看看它是如何用异步方式执行的。

当上面的代码加载在浏览器中,console.log('Hello World')被推到栈中然后当结束时从栈中被移除。然后,networkRequest( )触发,然后它被推到了栈的顶部。

接下来setTimeOut( )函数被调用,所以它被推到了栈顶。setTimeOut( )有两个参数:一是回调,二是毫秒数。

这个setTimeOut( )方法在web APIs环境中开始了一个2秒的计时器。此时,setTimeOut( )执行完毕并被移出栈。之后,console.log('The End')被推到栈里,执行完后被移除。

同时,计时器到期了,现在这个回调被推到了消息队列。但是这回调不会被立即执行,这里就是事件循环插手的地方。

事件循环

事件循环的作用是查看调用栈并确定调用栈是否为空。 如果调用栈为空,它会查看消息队列以查看是否有任何挂起的回调等待执行。

在上面的例子中,消息队列中有一个回调,并且此时调用栈已经空了。 因此,事件循环将回调推到栈顶部。

之后,console.log('Async Code')被推到栈顶部,执行完后从堆栈中弹出。 此时,回调已执行完毕,因此被从栈中移除,程序最终完成。

消息队列还包括了来自DOM事件的回调如点击事件和键盘事件等。例如:

在DOM事件的情况下,事件监听器在Web APIs环境中等待某个事件(在上述代码中是点击事件)发生,并且当该事件发生时,则回调函数会被放置在消息队列中等待执行。

然后事件循环检查调用栈是否为空,如果为空则将回调推到栈中,最后回调被执行。

推迟函数执行

我们还可以用setTimeOut来延迟函数执行直到栈是空的。例如:

上述代码的执行结果是: foo baz bar。

当代码开始运行时,foo( )首先被调用,在foo中我们调用了console.log('foo'),之后用bar( )作为回调和0秒的计时器来调用了setTimeOut( )。

如果这里我们没有使用setTImeout,bar( )函数会被立即执行,但使用了0秒的setTimeout帮助我们推迟了bar的执行直到栈清空了。

0秒过后bar( )被放进了消息队列等待被执行。但它只有等baz和foo函数执行完毕了栈清空了以后才会被执行。

ES6的任务队列

我们已经学习到了异步回调和DOM事件是如何使用消息队列来存储等待执行的回调。

ES6引入了在JavaScript中被Promises所使用的任务队列的概念。 消息队列和任务队列之间的区别在于任务队列的优先级高于消息队列,这意味着任务队列中的promise将在消息队列内的回调之前执行。举个例子:

上述代码会打印出:

我们能看到promise在setTImeout之前被执行,因为promise的响应所在的任务队列比消息队列的优先级高。

尾声

我们已经了解了异步JavaScript的工作原理以及调用栈、事件循环、消息队列和任务队列等其他概念,它们共同构成了JavaScript运行环境。 相信了解这些概念对你成为一名出色的JavaScript开发人员会很有帮助的!

Sort:  

Congratulations @pan-and-pear! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You made more than 10 comments. Your next target is to reach 50 comments.

Click here to view your Board of Honor
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Do not miss the last post from @steemitboard:

Meet the Steemians Contest - The results, the winners and the prizes
Meet the Steemians Contest - Special attendees revealed
Meet the Steemians Contest - Intermediate results

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @pan-and-pear! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You made more than 500 upvotes. Your next target is to reach 600 upvotes.

Click here to view your Board of Honor
If you no longer want to receive notifications, reply to this comment with the word STOP

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @pan-and-pear! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!