這篇文章是翻譯自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:)