本文2925字,閱讀大約須要10分鐘。javascript
總括: 本文梳理了異步代碼和同步代碼執行的區別,Javascript的事件循環,任務隊列微任務隊列等概念。前端
不曾失敗的人恐怕也不曾成功過。java
Javascript是單線程的編程語言,單線程就是說同一時間只能幹一件事。放到編程語言上來講,就是說Javascript引擎(執行Javascript代碼的虛擬機)同一時間只能執行一條語句。node
單線程語言的好處是你只管寫不用擔憂併發問題。但這也意味着沒法在不阻塞主線程的狀況下去執行一些諸如網絡請求的長時間操做。web
設想下若是咱們從某個接口請求一些數據,而後服務器須要一些時間才能將數據返回,此時就會阻塞主線程頁面處於無響應的狀態。編程
這裏就是Javascript異步的用武之地了,咱們能夠經過異步操做(好比回調函數,promise和async/await)來執行長時間的網絡請求而不阻塞主線程。promise
雖說了解這些全部的概念不必定讓你馬上成爲一名出色的Javascript開發者,但瞭解異步會對你頗有幫助。瀏覽器
話很少說,正文開始:)bash
在深刻研究Javascript的異步以前,咱們先來看下同步的代碼是如何在Javascript引擎中執行的。看例子:服務器
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); 複製代碼
要想理解上面的代碼是如何在Javascript引擎中被執行的,咱們必需要去理解Javascript的執行上下文和執行棧。
所謂的執行上下文是Javascript代碼執行環境中的一個抽象的概念。Javascript任何代碼都是在執行上下文中執行的。
函數內部的代碼會在函數執行上下文中執行,全局的代碼會在全局執行上下文中執行,每個函數都有本身的執行上下文。
顧名思義執行棧是一種後進先出(LIFO)的棧結構,它用來存儲在代碼執行階段建立的全部的執行上下文。
基於單線程的緣由,Javascript只有一個執行棧,由於是基於棧結構因此只能從棧的頂層添加或是刪除執行上下文。
讓咱們回到上面的代碼,嘗試理解Javascript引擎是如何去執行它們的。
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); 複製代碼
因此這裏發生了什麼呢?
當代碼被執行時,首先一個全局執行上下文(這裏用main()
表示)被建立而後壓到執行棧的頂端。當執行到first()
這一行代碼,它的執行上下文被壓到執行棧的頂端。
緊接着,console.log('Hi there!');
的函數執行上下文被壓到執行棧的頂端,執行結束後該執行上下文從執行棧彈出。而後調用second()
函數,該函數的執行上下文被壓到執行棧的頂端。
而後執行console.log('Hello there!');
,對應的函數執行上下文被壓入執行棧,執行結束被彈出,而後second()
函數執行結束,執行上下文被彈出。
console.log(‘The End’)
執行,函數執行上下文被壓入執行棧,執行結束被彈出,此時first()
函數執行結束,對應執行上下文被彈出。
整個程序執行結束,全局執行上下文(main())被彈出。
如今咱們已經對同步代碼的執行有了一個基本的認知,下面讓咱們看下異步代碼是如何執行的:
假設咱們用同步的方式去發起一個圖片請求或是一個普通的網絡請求,例子以下:
const processImage = (image) => { /** * doing some operations on image **/ console.log('Image processed'); } const networkRequest = (url) => { /** * requesting network resource **/ return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting(); 複製代碼
請求圖片或是網絡請求是須要花費時間的,所以當咱們調用processImage()
的時候,花費的時間取決於圖片的大小。
當processImage()
函數執行結束,響應的執行上下文從執行棧中彈出,而後調用networkRequest()
函數,對應執行上下文被壓入執行棧,該函數一樣須要花費一些時間才能結束。
networkRequest()
函數執行結束,調用greeting()
,而後裏面只有一行console.log('Hello World')
,``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++ API(nodejs中)的一部分。
事件循環、Web API和消息隊列/任務隊列並非Javascript引擎的一部分而是瀏覽器的Javascript運行環境或是Nodejs的Javascript運行環境的一部分,在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
有兩個參數:1. 回調函數;2. 時間(以毫秒ms爲單位);3. 附加參數(會被傳到回調函數裏面)
setTimeout()
函數會在web API運行環境中進行一個2s
的倒計時,這個時候 setTimeout()
函數就已經執行完了,執行上下文從執行棧中彈出。再而後console.log('The End')
函數被執行,進入執行棧,結束後彈出執行棧。
這時候倒計時到期,setTimeout()
的回調函數被推到消息隊列中,但回調函數不會當即執行,這是事件循環開始的地方。
事件循環的工做就是去查看執行棧,肯定執行棧是否爲空,若是執行棧爲空,那麼就去檢查消息隊列,看看消息隊列中是否有待執行的回調函數。它按照相似以下的方式來被實現:
while (queue.waitForMessage()) { queue.processNextMessage(); } 複製代碼
在這裏,執行棧已經爲空,消息隊列包含一個setTimeout
函數的回調函數,所以事件循環把回調函數的執行上下文壓入執行棧的頂端。
而後console.log(‘Async Code’)
函數的執行上下文被壓入執行棧,結束後從執行棧彈出。這時候回調函數執行結束,對應的執行上下文也從執行棧中彈出。
**消息隊列(也叫任務隊列)**中也會包含來自DOM事件(好比點擊事件,鍵盤事件等),看例子:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); }); 複製代碼
對於DOM事件來講,web API中會有一個事件偵聽器堅挺某個事件被觸發(在這裏是click事件),當某個事件被觸發時,就會把相應的回調函數放入消息隊列中執行。
事件循環再次檢查執行棧,若是執行棧爲空,就把事件的回調函數推入執行棧。
咱們已經瞭解了異步回調和事件回調是如何執行的,這些回調函數被存儲在消息隊列中等待被執行。
ES6中爲promise
函數引入了微任務隊列(也叫做業隊列)的概念。微任務隊列和消息隊列的區別就是優先級上的區別,微任務隊列的優先級要高於消息隊列。也就是說在微任務隊列的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
的response被存儲在微任務隊列中,有比消息隊列更高的優先級。
再看另外一個例子,有兩個promise
函數,兩個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了,而後這個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中同步和異步代碼是怎麼執行,以及一些其它的概念(包括執行棧,事件循環,微任務隊列,消息隊列等)。
以上。
能力有限,水平通常,歡迎勘誤,不勝感激。
訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍