息息相關的 JS 同步,異步和事件輪詢

做者:Sukhjinder Arorajavascript

譯者:前端小智html

來源:medium前端


阿里雲最近在作活動,低至2折,有興趣能夠看看promotion.aliyun.com/ntms/yunpar…java


爲了保證的可讀性,本文采用意譯而非直譯。c++

JS 是一門單線程的編程語言,這就意味着一個時間裏只能處理一件事,也就是說JS引擎一次只能在一個線程裏處理一條語句。git

雖然單線程簡化了編程代碼,由於這樣我們沒必要太擔憂併發引出的問題,這也意味着在阻塞主線程的狀況下執行長時間的操做,如網絡請求。github

想象一下從API請求一些數據,根據具體的狀況,服務器須要一些時間來處理請求,同時阻塞主線程,使網頁長時間處於無響應的狀態。這就是引入異步 JS 的緣由。使用異步 (如 回調函數、promiseasync/await),能夠不用阻塞主線程的狀況下長時間執行網絡請求。web

瞭解異步的工做方式以前,我們先來看看同步是怎麼樣工做的。編程

同步 JS 是如何工做的?

在深刻研究異步JS以前,先來了解同步 JS 代碼在 JavaScript 引擎中執行狀況。例如:api

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

複製代碼

要理解上述代碼如何在 JS 引擎中執行,我們必須理解什麼是執行上下文調用棧(也稱爲執行堆棧)。

函數代碼在函數執行上下文中執行,全局代碼在全局執行上下文中執行。每一個函數都有本身的執行上下文。

調用棧

調用堆棧顧名思義是一個具備LIFO(後進先出)結構的堆棧,用於存儲在代碼執行期間建立的全部執行上下文。

JS 只有一個調用棧,由於它是一種單線程編程語言。調用堆棧具備 LIFO 結構,這意味着項目只能從堆棧頂部添加或刪除。

回到上面的代碼,嘗試理解代該碼是如何在JS引擎中執行。

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(),所以second()函數被推到堆棧的頂部。

console.log('Hello there!')被推送到堆棧頂部,並在完成時彈出堆棧。second() 函數結束,所以它從堆棧中彈出。

console.log(「the End」)被推到堆棧的頂部,並在完成時刪除。以後,first()函數完成,所以從堆棧中刪除它。

程序在這一點上完成了它的執行,因此全局執行上下文(main())從堆棧中彈出。

異步 JS 是如何工做的?

如今我們已經對調用堆棧和同步JAS的工做原理有了基本的瞭解,回到異步JS上。

阻塞是什麼?

假設我們正在以同步的方式進行圖像處理或網絡請求。例如:

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()函數。

所以,我們必須等待函數如processImage()networkRequest()完成。這意味着這些函數阻塞了調用堆棧或主線程。所以,在執行上述代碼時,我們不能執行任何其餘操做,這是不理想的。

解決辦法是什麼?

最簡單的解決方案是異步回調,各位使用異步回調使代碼非阻塞。例如:

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

這裏使用了setTimeout方法來模擬網絡請求。請記住setTimeout不是JS引擎的一部分,它是Web Api的一部分。

爲了理解這段代碼是如何執行的,我們必須理解更多的概念,好比事件輪詢和回調隊列(或消息隊列)。

事件輪詢、web api和消息隊列不是JavaScript引擎的一部分,而是瀏覽器的JavaScript運行時環境或Nodejs JavaScript運行時環境的一部分(對於Nodejs)。在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. 回調和
    1. 以毫秒(ms)爲單位的時間。

setTimeout() 方法在web api環境中啓動一個2s的計時器。此時,setTimeout()已經完成,並從堆棧中彈出。cosole.log(「the end」) 被推送到堆棧中,在完成後執行並從堆棧中刪除。

同時,計時器已通過期,如今回調被推送到消息隊列。可是回調不會當即執行,這就是事件輪詢開始的地方。

事件輪詢

事件輪詢的工做是監聽調用堆棧,並肯定調用堆棧是否爲空。若是調用堆棧是空的,它將檢查消息隊列,看看是否有任何掛起的回調等待執行。

在這種狀況下,消息隊列包含一個回調,此時調用堆棧爲空。所以,事件輪詢將回調推到堆棧的頂部。

而後是 console.log(「Async Code」) 被推送到堆棧頂部,執行並從堆棧中彈出。此時,回調已經完成,所以從堆棧中刪除它,程序最終完成。

消息隊列還包含來自DOM事件(如單擊事件和鍵盤事件)的回調。例如:

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

對於DOM事件,事件偵聽器位於web api環境中,等待某個事件(在本例中單擊event)發生,當該事件發生時,回調函數被放置在等待執行的消息隊列中。

一樣,事件輪詢檢查調用堆棧是否爲空,並在調用堆棧爲空並執行回調時將事件回調推送到堆棧。

延遲函數執行

我們還可使用setTimeout來延遲函數的執行,直到堆棧清空爲止。例如

const bar = () => {
  console.log('bar');
}
const baz = () => {
  console.log('baz');
}
const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  baz();
}
foo();
複製代碼

打印結果:

foo
baz
bar
複製代碼

當這段代碼運行時,第一個函數foo()被調用,在foo內部咱們調用console.log('foo'),而後setTimeout()被調用,bar()做爲回調函數和時0秒計時器。

如今,若是我們沒有使用 setTimeout,bar() 函數將當即執行,可是使用 setTimeout0秒計時器,將bar的執行延遲到堆棧爲空的時候。

0秒後,bar()回調被放入等待執行的消息隊列中,可是它只會在堆棧徹底空的時候執行,也就是在bazfoo函數完成以後。

ES6 任務隊列

咱們已經瞭解了異步回調和DOM事件是如何執行的,它們使用消息隊列存儲等待執行全部回調。

ES6引入了任務隊列的概念,任務隊列是 JS 中的 promise 所使用的。消息隊列和任務隊列的區別在於,任務隊列的優先級高於消息隊列,這意味着任務隊列中的promise 做業將在消息隊列中的回調以前執行,例如:

const bar = () => {
  console.log('bar');
};

const baz = () => {
  console.log('baz');
};

const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  new Promise((resolve, reject) => {
    resolve('Promise resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));
  baz();
};

foo();
複製代碼

打印結果:

foo
baz
Promised resolved
bar
複製代碼

我們能夠看到 promisesetTimeout 以前執行,由於 promise 響應存儲在任務隊列中,任務隊列的優先級高於消息隊列。

小結

所以,我們瞭解了異步 JS 是如何工做的,以及調用堆棧、事件循環、消息隊列和任務隊列等概念,這些概念共同構成了 JS 運行時環境。雖然成爲一名出色的JS開發人員並不須要學習全部這些概念,可是瞭解這些概念是有幫助的。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:blog.bitsrc.io/understandi…

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索