1. 程式人生 > >非同步 JavaScript - 事件迴圈

非同步 JavaScript - 事件迴圈

簡評:如果你對 JavaScript 非同步的原理感興趣,這裡有一篇不錯的介紹。

JavaScript 同步程式碼是如果工作的

在介紹 JavaScript 非同步執行之前先來了解一下, JavaScript 同步程式碼是如何執行的。

這裡有兩個概念需要了解:

** 執行上下文(Excution Context)**

執行上下文是一個抽象的概念,用於表示 JavaScript 的執行環境,任何程式碼都會有一個執行上下文。

全域性程式碼執行在全域性執行上下文,函式裡的程式碼執行在函式執行上下文,每一個函式都有自己的執行上下文。

呼叫堆疊(Call Stack)

呼叫棧是一個具有 LIFO(後進先出)結構的棧,用於儲存程式碼執行階段所有的執行上下文。

因為 JavaScript 是單執行緒的,所以 JavaScript 只有一個單獨的呼叫棧。

我們以下面例子介紹同步程式碼執行過程。

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

建立全域性上下文(由 main() 表示),並將全域性上下文推到棧頂。然後依次將遇到函式執行上下文推到棧頂(如果函式中執行其他他函式,其他函式依次推到棧頂以此類推)。當函式執行完畢對應的執行上下文會從呼叫棧彈出,程式結束時全域性上下文從呼叫棧彈出。

JavaScript 非同步程式碼是如何執行的?

通過上個章節我們已經對呼叫棧和 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 中的任務執行。

原文:Understanding Asynchronous JavaScript — the Event Loop