簡評:若是你對 JavaScript 異步的原理感興趣,這裏有一篇不錯的介紹。javascript
在介紹 JavaScript 異步執行以前先來了解一下, JavaScript 同步代碼是如何執行的。java
這裏有兩個概念須要瞭解:瀏覽器
** 執行上下文(Excution Context)**異步
執行上下文是一個抽象的概念,用於表示 JavaScript 的運行環境,任何代碼都會有一個執行上下文。async
全局代碼運行在全局執行上下文,函數裏的代碼運行在函數執行上下文,每個函數都有本身的執行上下文。函數
調用堆棧(Call Stack)oop
調用棧是一個具備 LIFO(後進先出)結構的棧,用於儲存代碼執行階段全部的執行上下文。線程
由於 JavaScript 是單線程的,因此 JavaScript 只有一個單獨的調用棧。code
咱們如下面例子介紹同步代碼執行過程。blog
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
建立全局上下文(由 main() 表示),並將全局上下文推到棧頂。而後依次將遇到函數執行上下文推到棧頂(若是函數中執行其餘他函數,其餘函數依次推到棧頂以此類推)。當函數執行完畢對應的執行上下文會從調用棧彈出,程序結束時全局上下文從調用棧彈出。
經過上個章節咱們已經對調用棧和 JavaScript 的同步執行有了基本的瞭解,如今來看看 JavaScript 異步執行是如何工做的。
什麼是阻塞?
因爲 JavaScript 是單線程的,若是某個函數耗費的時間比較長,會阻塞後面的任務執行,這就形成了阻塞。解決阻塞最簡單的方法是函數直接返回不等待,使用異步回調來處理返回結果。
在瞭解 JavaScript 異步執行以前還須要知道一些概念,事件循環和回調隊列(也稱爲任務隊列或消息隊列)。
注意:Event Loop 、Web APIs 和 Message Queue 並非 JavaScript 引擎的一部分,而是瀏覽器運行時環境和 Nodejs 運行時環境的一部分。
咱們如下面代碼爲例,解釋異步代碼是如何執行的。
const networkRequest =()=> { setTimeout(()=> { console.log('Async Code'); },2000); }; console.log('Hello World'); networkRequest(); console.log('The End');
當上述程序加載到瀏覽器時 console.log(‘Hello World’) 代碼執行時會一次在調用棧推入和彈出。遇到 networkRequest() 將其推入到調用棧頂。而後繼續將 networkRequest 內的 setTimeout 方法推入棧頂,隨後 setTimeout networkRequest 依次出棧。最後對 console.log(‘The End’) 進行入棧出棧。
當 timer 到期後會將 callback 推入 message queue(消息隊列)中,此時 callback 不會立刻執行。會等待事件循環調度。
事件循環
事件循環的做用是查看調用棧並肯定調用棧是否空閒。若是調用棧空閒,even loop 會查看消息隊列是否有待處理的 callback 須要觸發。例子中的消息隊列只包含一個 callback,當調用棧爲空的時候,even loop 會將 callback 推入調用棧中觸發 networkRequest 的回調。
DOM 事件
消息隊列還會包含來自 DOM 的事件回調,好比鼠標和鍵盤事件回調。例如:
document.querySelector('.btn').addEventListener('click',function callback(event) { console.log('Button Clicked'); });
對於 DOM 事件,當具體的事件觸發會將 callback 推入消息隊列中,等待 even loop 來調度執行。
ES6 job queue/micro-task queue
ES6 新增了 job queue/micro-task queue 概念,在 Promise 中用到。job queue 比 message queue 擁有更高的優先級。意味着 job queue 和 message queue 都有任務時會優先執行 job queue 中的任務。例如:
console.log('Script start'); // callback 在 message queue 中 setTimeout(function callback() { console.log('setTimeout'); }, 0); // 任務在 micro-task queue 中 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
再來看下一個例子(兩個 setTimeout 和 兩個 Promise):
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
因而可知 micro-task queue 中的全部任務都會優先於 message queue 中的任務執行。