这篇文章是翻译自Sukhjinder Arora的
Understanding Asynchronous JavaScript。这篇文章描述了异步和同步JavaScript是如何在运行环境中,使用调用栈,消息队列,做业队列,以及事件循环来工做的。文章若有翻译很差的地方还望多多包涵。javascript
众所周知,JavaScript 是单线程的编程语言,那就意味着在同一个时间只能有一件事发生。通俗的讲,JavaScript引擎每个线程一次只能处理一个声明。java
虽然单线程语言能够简化写代码的过程,由于你不用担忧并发的问题,但这样同时也意味着你没法在不锁住主线程的状况下,执行像网络访问这种长时间的操做。node
想象一下,从API请求数据的这个状况。服务器可能须要一些时间来处理请求,同时阻塞主线程使网页无响应。编程
这就是异步Javascript能够发挥做用的地方了。使用异步JavaScript(例如像回调,promises,和async/await),你就能够在不锁住主线程的状况下执行长时间的网络请求。promise
你没有必要学习全部这些概念来成为一个出色JavaScript工程师,这些只是对你颇有帮助而已:)浏览器
因此废话很少说,咱们开始吧。服务器
在咱们深刻了解异步JavaScript以前,让咱们先来了解一下同步的JavaScript代码是如何在引擎内部执行的。举个例子:网络
const second = () => { console.log('hello there'); } const first = () => { console.log('hi,there'); second(); console.log('The End'); } first();
在咱们想要理解上面代码是如何在JavaScript引擎执行的以前,咱们须要先要理解执行上下文和调用栈的概念(也叫执行栈)。并发
执行上下文是JavaScript代码被评估和执行的地方的抽象概念。每当任何js代码执行的时候,他们就运行在执行上下文内部。dom
函数执行在函数的执行上下文内,全局代码执行在全局的执行上下文内。每一个函数都有本身的执行上下文。
调用栈就像他名字里展现的那样,他是一个具备后进先出的栈结构,它用于存储代码执行期间建立的全部执行上下文。
JavaScript是拥有单一调用栈的,由于它是单线程的语言。调用栈的LIFO(后进先出结构)决定了东西只能从栈的顶部添加或者删除。
让咱们回到上面的代码片断,而后尝试理解一下上面的代码片断是怎么在JavaScript引擎内部执行的。
const second = () => { console.log('hello there'); } const first = () => { console.log('hi,there'); second(); console.log('The End'); } first();
上面代码的调用栈:)
当代码执行的时候,一个全局的执行上下文就被建立了(表示为main()
)而后将他压入调用栈的顶部。当first()
被调用的时候,first()
又被压入调用栈的顶部。
接下来,console.log('hi,there')
又被压入栈的顶部,当它执行结束,他就从栈中弹出了。以后,咱们调用了second()
,因此second()
函数就被压入栈顶。
console.log('Hello there!')
被压入栈顶,而且当它执行结束就被弹出。 此时,second()
函数执行结束,因此从栈中弹出。
console.log('The End')
被压入栈顶而后再结束的时候被移出。而后,first()
函数执行结束,被移出调用栈。
此时,整个程序结束调用,因此全局执行上下文(main()
)从栈中弹出。
如今咱们已经对调用栈有个大体了解了,也知道了同步的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()
函数结束,greeting()
函数被调用,由于他只包含一个console.log
声明,并且console.log
声明执行的很是地块,因此greeting()
函数很快的就结束调用了。
如你所见,咱们必需要等,等到函数(就像processImage()
和networkRequest()
)结束执行。这就意味着这些函数被锁在调用栈或者主线程里。 因此在上述代码执行期间咱们不能执行任何其余的操做,这不毫不是咱们想要的。
最简单的解决办法就是异步回调。咱们使用异步回调让咱们的代码不被锁住。举个栗子:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); },2000); }; console.log('Hello World'); networkRequest();
在这里我使用了setTimeout
方法来模拟网络请求。请注意setTimeout
不是Javascript引擎的一部分,它是Web Api(浏览器中)和 C/C++ (在node.js)中的一部分。
为了理解这段代码是如何执行的,咱们须要理解更多的概念,好比像事件循环和回调队列(也叫作任务队列或者消息队列)。
事件循环,WEB API, 消息队列/任务队列不是JavaScript引擎的一部分,他们是浏览器的JavaScript运行时环境或者Node.js JavaScript 运行环境的一部分。 在Nodejs中,网络接口被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)以ms为单位的时间。setTimeout
在Web API环境中开始了一个为时2s的计时器。此时,setTimeout
已经结束了,因此被弹出栈,接着,console.log('The End')
被压入栈,执行而后在结束后从栈中移出。
与此同时,计时器到时间了,如今回调被推入到信息队列,但回调并无被当即执行,而是被放到了事件循环开始的地方。
事件循环的责任就是查看调用栈并肯定调用栈是否为空。若是调用栈为空,他就会查看消息队列来肯定是否有任何挂起的回调函数等待被执行。
在这个例子中消息队列中包括一个回调函数,而且此时调用栈为空。所以事件循环把回调函数压入栈顶。
在那以后,console.log(‘Async Code‘)
这条语句被压入栈顶,执行,而后从栈中弹出。此时回调函数结束了,因此它被从栈中弹出,而后整个程序结束执行。
消息队列中也包括DOM事件中的回调函数好比点击事件和键盘事件,例如:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); })
在DOM事件里,事件监听器位于Web API 环境中等待某个事件发生(在这个例子中是点击事件),而且当该事件发生的时候,回调函数则被放置在消息队列中等待被执行。
事件循环会再次检查调用栈是否为空,若是为空的话,它会把事件回调压入栈中,而后回调函数则被执行。
咱们已经学习了异步回调和DOM 事件是如何执行的,他们使用消息队列来存储全部等待被执行的回调。
ES6介绍了一种被JavaScript 中Promises使用的叫作做业队列/微任务队列的概念。消息队列和做业队列的区别就在于做业队列会比消息队列拥有更高的优先级,也就是说做业队列/微任务队列中的Promise的任务会比消息队列中的回调函数先执行。
例如:
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的返回是存储在微任务队列中的,它比消息队列拥有更高的优先级。
让咱们看下一个例子,此次有两个Promises和两个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
咱们能够看到两个promise都是在setTimeout回调的前面执行的,由于事件循环机制中,微任务队列中的任务要优先于消息队列/任务队列中的任务。
当事件循环正在执行微任务队列中的任务时,若是另外一个promise处于resolved的状态的话,他会被添加到同一个微任务队列的尾部,而且他会比消息队列中的回调先执行,无论回调函数已经等待执行了多久了。(优先级高果真就是能随心所欲= =)。
举个例子:
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的运行环境。再重申一下,虽然您没有必要将这些全部的概念都学习,来成为一个出色的JavaScript开发人员,但了解这些概念会颇有帮助:)
今天的文章就这样啦,若是你以为这篇文章对你颇有帮助,请点击旁边的鼓掌按钮,你也能够在Medium和Twitter上面follow我。若是你有任何的疑问,欢迎在下面留言,我会很开心的帮助你的:)
若是你对个人翻译或者内容有什么意见或者建议欢迎在下面留言告诉我,喜欢文章就给个赞吧,很是感谢您的阅读,Hava a nice day:)