息息相關的 JS 同步,非同步和事件輪詢
- 2019 年 10 月 4 日
- 筆記
作者:Sukhjinder Arora 譯者:前端小智 來源:medium
為了保證的可讀性,本文採用意譯而非直譯。
JS 是一門單執行緒的程式語言,這就意味著一個時間裡只能處理一件事,也就是說JS引擎一次只能在一個執行緒里處理一條語句。
雖然單執行緒簡化了編程程式碼,因為這樣咱們不必太擔心並發引出的問題,這也意味著在阻塞主執行緒的情況下執行長時間的操作,如網路請求。
想像一下從API請求一些數據,根據具體的情況,伺服器需要一些時間來處理請求,同時阻塞主執行緒,使網頁長時間處於無響應的狀態。這就是引入非同步 JS 的原因。使用非同步 (如 回調函數、promise、async/await),可以不用阻塞主執行緒的情況下長時間執行網路請求。
了解非同步的工作方式之前,咱們先來看看同步是怎麼樣工作的。
同步 JS 是如何工作的?
在深入研究非同步JS之前,先來了解同步 JS 程式碼在 JavaScript 引擎中執行情況。例如:
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) 回調和
- 2) 以毫秒(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() 函數將立即執行,但是使用 setTimeout 和0秒計時器,將bar的執行延遲到堆棧為空的時候。
0秒後,bar()回調被放入等待執行的消息隊列中,但是它只會在堆棧完全空的時候執行,也就是在baz和foo函數完成之後。
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
咱們可以看到 promise 在 setTimeout 之前執行,因為 promise 響應存儲在任務隊列中,任務隊列的優先順序高於消息隊列。
小結
因此,咱們了解了非同步 JS 是如何工作的,以及調用堆棧、事件循環、消息隊列和任務隊列等概念,這些概念共同構成了 JS 運行時環境。雖然成為一名出色的JS開發人員並不需要學習所有這些概念,但是了解這些概念是有幫助的。
程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
原文:https://blog.bitsrc.io/unders...
交流
乾貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。