理解JavaScript異步編程【譯】

原文連接:blog.bitsrc.io/understandi…
做者主頁:blog.bitsrc.io/@Sukhjinderjavascript

JavaScript是單線程編程語言,這意味着同一時間只能發生一件事情。也就是說,JavaScript引擎只能在一個線程的同一時間裏處理一個語句。java

單線程語言簡化了咱們的編程,由於你不用擔憂併發問題,但這也意味着在執行像網絡請求這樣耗時的操做的時候,會堵塞主進程的進行。node

想象着從一個API接口請求一些數據,在某些狀況下服務器會花費一些時間處理請求,遲遲沒有給出響應,這樣就阻塞了主進程,讓網頁變得遲鈍。web

這就是異步JavaScript發揮做用的地方。用異步的JavaScript(好比回調函數,promise或者async/await),你能夠在不阻塞主進程的狀況下執行長網絡請求。編程

雖然你沒必學習全部的概念去成爲一名優秀的JavaScript工程師,但瞭解這些概念仍是頗有幫助的。promise

那麼廢話很少說了,讓咱們進入正題吧!瀏覽器

同步的JavaScript是怎麼工做的?

在咱們深刻了解異步JavaScript以前,讓咱們先理解同步JavaScript在JavaScript引擎裏是怎麼執行的,舉個例子:bash

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

爲了理解上述代碼是怎麼在JavaScript引擎裏執行的,咱們必須瞭解執行上下文和調用棧(也被稱爲執行棧)的概念。服務器

執行上下文(Execution Context)

執行上下文是JavaScript代碼在一個環境中編譯和執行的抽象概念。在JavaScript裏運行的任何代碼,都會在執行上下文中執行。 函數裏的代碼在函數執行上下文中執行,全局代碼在全局執行上下文中執。每個函數都有它本身的執行上下文。網絡

執行棧(call stack)

執行棧,顧名思義,就是一個後進先出的棧結構,用來在代碼運行的時候存儲全部被建立的執行上下文。

JavaScript有一個單獨的執行棧,由於它是單線程編程語言。執行棧是後進先出的數據結構就意味着元素只能出棧頂被增長或者移除。

讓咱們回到剛纔提到的代碼段,試着去理解一下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的工做原理,讓咱們回到異步JavaScript上面。

什麼是阻塞(blocking)?

假設咱們正在用同步的方式進行圖像處理或網絡請求:

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()函數被調用,壓入執行棧。一樣的,須要花費一些時間去結束這個函數的執行。

因此你看,咱們必須得等到函數(好比processImage()或networkRequest())執行完了,才能進行下一步動做。這也意味着這些函數會阻塞執行棧或者主進程,咱們沒法在執行這些函數的同時進行一些其餘的操做,這是很不科學的。

如何解決這個問題呢?

最簡單的解決辦法就是使用異步回調了。咱們使用異步的回調函數讓代碼再也不被阻塞,好比:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();
複製代碼

這裏我用一個定時器方法去模仿網絡請求。請牢記,setTimeout定時器並非JavaScript引擎的一部分,而是web APIs(在瀏覽器中)和C/C++ APIs(在node.js中)的一部分。

爲了理解這段代碼是如何執行的,咱們還得理解一些別的概念,好比事件循環(Event Loop)和回調隊列(Message Quene,也被稱爲任務隊列或消息隊列)。

事件循環,web APIs 和消息(任務)隊列不是JavaScript引擎的一部分,他們是瀏覽器JavaScript運行環境或者Nodejs JavaScript運行環境的一部分。在Nodejs中,web APIs被C/C++ APIs取代。
如今,讓咱們回到剛纔的代碼來看一下它是怎麼用異步的方式執行的吧。

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');
複製代碼

輸出:

Hello World
The End
Async Code
複製代碼

當上述代碼運行在瀏覽器中,console.log('Hello World')被壓入棧裏,執行完畢後,從棧裏彈出。以後,調用networkRequest(),把這個函數壓入棧頂。
以後執行定時器setTimeout()這個函數,把setTimeout()壓入棧頂。這個函數有兩個參數,第一個是回調函數,第二個是以毫秒爲單位的時間。
setTimeout()函數在web APIs這個環境開啓了一個2秒的定時器。這時,setTimeout()函數執行結束了,從棧頂彈出。
以後,console.log('The End')被壓入棧頂,執行結束後,從棧頂彈出。
而後,定時器到時間過時了,這個回調函數被壓入了消息隊列裏。可是回調函數沒有當即執行,而是回到了事件循環開始的地方。

事件循環(Event Loop)

事件循環的工做就是去查看執行棧是否是空的。若是執行棧是空的,事件循環就去消息隊列裏查看是否有等待被執行的回調函數。
在本例裏,消息隊列包含一個等待被執行的回調函數,執行棧是空的。事件循環就會把回調函數壓到執行棧棧頂去。
在console.log('Async Code')這段代碼被壓入執行棧棧頂,執行完畢而後從棧頂彈出以後。以後這個回調函數從棧頂彈出。到這時,這段程序纔算真正地結束了。

DOM 事件

消息隊列也包含來自DOM事件的回調函數,好比點擊事件和鍵盤事件,例如:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});
複製代碼

在DOM事件裏,事件監聽器在web APIs環境裏等待一個具體事件發生(在這個例子裏是點擊事件),當那個事件發生後,回調函數就被放到消息隊列裏等待被執行。
一樣的,事件循環檢查執行棧是否是空的,若是是空的,就把回調函數壓入執行棧裏,讓回調函數執行。
如今,咱們已經瞭解到怎麼執行異步的回調函數和DOM事件了,顯然,他們是被消息隊列存起來而後再等待被執行的。

ES6 工做隊列/微任務隊列

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的返回是存儲在微任務隊列裏的,因此它的執行優先級比消息隊列高。 讓咱們看另外一個例子,這個例子裏有兩個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,它會被加到同一個微任務隊列的最後面去,並且它會在消息隊列裏的回調函數執行以前執行,無論要等多久,回調函數只能等着微任務執行完了才執行。例如:

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工程師,但瞭解這些概念仍是對你有幫助的。 :)

相關文章
相關標籤/搜索