天天看點

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

原文

使用像 JavaScript 這樣的語言進行程式設計時,最重要但也經常被誤解的部分之一是如何表達和操作一段需要某段時間才能完成執行的程式行為。

這不僅僅是從 for 循環開始到 for 循環結束發生的事情,這當然需要一些時間(微秒到毫秒)才能完成。它是關于當你的程式的一部分現在運作而你的程式的另一部分稍後運作時會發生什麼。在程式的兩部分分别得到執行的時間間隙,存在着一個 gap.

實際上,所有編寫過的重要程式(尤其是用 JS 編寫的)都必須以某種方式管理這個 gap,無論是等待使用者輸入、從資料庫或檔案系統請求資料、通過網絡發送資料以及等待響應,或以固定的時間間隔執行重複的任務(如動畫)。通過所有這些不同的方式,您的程式必須及時管理狀态。

異步程式設計從 JS 開始就已經存在了,這是肯定的。但是大多數 JS 開發人員從來沒有真正仔細考慮過它是如何以及為什麼會出現在他們的程式中,或者探索各種其他方法來處理它。足夠好的方法一直是不起眼的回調函數。直到今天,許多人仍堅持認為回調已綽綽有餘。

但是随着 JS 的範圍和複雜性不斷增長,為了滿足在浏覽器和伺服器以及介于兩者之間的所有可能的裝置中運作的一流程式設計語言不斷擴大的需求,我們管理異步的痛苦正變得越來越嚴重,他們迫切需要更有能力和更合理的方法。

我們必須更深入地了解異步是什麼以及它如何在 JS 中運作。

a program in chunks

你可以在一個 .js 檔案中編寫你的 JS 程式,但你的程式幾乎肯定由幾個塊組成,其中隻有一個現在要執行,其餘的将稍後執行。 每個塊最常見的機關是函數。

大多數剛接觸 JS 的開發人員似乎都有的問題是,“later”不會嚴格地發生在“now”之後。 換句話說,根據定義,目前無法完成的任務将異步完成,是以我們不會像您直覺地期望或想要的那樣有阻塞行為。

考慮:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

您可能知道标準 Ajax 請求不是同步完成的,這意味着 ajax(…) 函數還沒有任何傳回值以配置設定給 data 變量。 如果 ajax(…) 可以阻塞直到響應回來,那麼 data = … 指派會正常工作。

但這不是我們使用 Ajax 的方式。 我們現在發出一個異步的 Ajax 請求,直到稍後我們才會得到結果。

從現在到以後“等待”的最簡單(但絕對不僅僅是,甚至最好!)方法是使用一個函數,通常稱為回調函數:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

看這段代碼:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

這個程式有兩個部分:現在将運作的内容和稍後運作的内容。 這兩個塊是什麼應該很明顯。

現在立即執行的代碼塊:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

稍後異步執行的代碼塊:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

一旦您執行程式, now 塊就會立即運作。 但是 setTimeout(…) 也會設定一個事件(逾時)稍後發生,是以 later() 函數的内容将在稍後的時間(從現在起 1,000 毫秒)執行。

任何時候您将一部分代碼包裝到一個函數中并指定它應該響應某些事件(計時器、滑鼠單擊、Ajax 響應等)而執行時,您正在建立代碼的“later”部分,進而引入異步到你的程式。

Async Console

console.log 到底是同步輸出還是異步輸出?

沒有關于 console.* 方法如何工作的規範或一組要求——它們不是 JavaScript 的正式組成部分,而是由托管環境添加到 JS 中。

是以,不同的浏覽器和 JS 環境有着各自的實作,這有時會導緻混亂的行為。

特别是,有一些浏覽器和一些條件,console.log(…) 實際上并沒有立即輸出它給出的内容。 這可能發生的主要原因是因為 I/O 是許多程式(不僅僅是 JS)的一個非常緩慢和阻塞的部分。 是以,浏覽器在背景異步處理控制台 I/O 可能會表現得更好(從頁面/UI 角度來看),而您甚至可能不知道發生了這種情況。

一個不太常見但可能的場景,可以觀察到這種情況(不是從代碼本身而是從外部):

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

我們通常希望在 console.log(…) 語句的确切時刻看到 a 對象被快照,列印類似 { index: 1 } 的内容,這樣在 a.index++ 發生時的下一個語句中,它正在修改與 a. 的輸出不同的東西,或者完全不同的東西。

大多數情況下,前面的代碼可能會在您的開發人員工具的控制台中生成您所期望的對象表示。但同樣的代碼可能會在浏覽器認為需要将控制台 I/O 推遲到背景的情況下運作,在這種情況下,當對象在浏覽器控制台中表示時,a.index++已經發生了,它顯示 { index: 2 }。

在什麼條件下控制台 I/O 将被推遲,甚至是否可以觀察到,這是一個不斷變化的目标。請注意 I/O 中這種可能的異步性,以防您在調試中遇到問題,其中在 console.log(…) 語句之後修改了對象,但您看到意外的修改出現。

Event Loop

讓我們做出一個(也許令人震驚的)聲明:盡管您顯然能夠編寫異步 JS 代碼(例如我們剛剛看到的逾時),但直到最近(ES6),JavaScript 本身實際上從未有任何内置的異步的直接概念.

什麼!?這似乎是一個瘋狂的主張,對吧?事實上,這是非常正确的。 JS 引擎本身從來沒有做過任何事情,隻是在任何給定的時刻,在被要求時執行你的程式的單個塊。

被誰要求執行呢?這個問題很關鍵。

JS 引擎不是孤立運作的。它在托管環境中運作,對于大多數開發人員來說,這是典型的 Web 浏覽器。在過去的幾年裡(但絕不是唯一的),JS 通過 Node.js 之類的東西從浏覽器擴充到其他環境,例如伺服器。事實上,如今 JavaScript 被嵌入到各種裝置中,從機器人到燈泡。

但是所有這些環境的一個共同“線程”是它們中有一種機制來處理随着時間的推移來執行多個程式塊,在每個時間點調用JS 引擎。這個線程稱為事件循環。

換句話說,JS 引擎并沒有與生俱來的時間感,而是一個任意 JS 片段的按需執行環境。總是安排“事件”(即 JS 代碼執行)的是執行 JavaScript 代碼的托管環境。

是以,例如,當您的 JS 程式發出 Ajax 請求以從伺服器擷取一些資料時,您在函數中設定響應代碼(通常稱為回調),JS 引擎告訴托管環境,“嘿,我現在将暫停執行,但是每當您完成該網絡請求并且您有一些資料時,請回調此函數。”

然後浏覽器被設定為監聽來自網絡的響應,當它有東西給你時,浏覽器将回調函數插入到事件循環中,以此來排程回調函數的執行。

那麼什麼是事件循環呢?

讓我們首先通過一些假代碼來概念化它。

事件循環(event loop)的邏輯可以用下面的僞代碼來表示:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

當然,這是為了說明概念而大大簡化的僞代碼。但這應該足以幫助獲得更好的了解。

如您所見,while 循環代表了一個持續運作的循環,該循環的每次疊代稱為一個滴答。對于每個滴答聲,如果一個事件在隊列中等待,它就會被從隊列裡摘下并執行。這些事件是您的函數回調。

重要的是要注意 setTimeout(…) 不會将您的回調放在事件循環隊列中。它的作用是設定一個計時器;當計時器到期時,環境會将您的回調放入事件循環中,以便将來某個滴答聲将其拾取并執行。

如果此時事件循環中已經有 20 個項目怎麼辦?您的回調等待。它排在其他人後面——通常沒有用于搶占隊列和跳過隊列的路徑。這解釋了為什麼 setTimeout(…) 計時器可能無法以完美的時間精度觸發。您可以保證(粗略地說)您的回調不會在您指定的時間間隔之前觸發,但它可以在該時間或之後發生,具體取決于事件隊列的狀态。

是以,換句話說,您的程式通常被分解成許多小塊,這些小塊在事件循環隊列中一個接一個地發生。從技術上講,與您的程式不直接相關的其他事件也可以在隊列中交錯。

Parallel Threading

将術語“異步 async”和“并行 parallel”混為一談是很常見的,但它們實際上是完全不同的。 請記住,異步是關于現在和以後之間的gap. 但并行是指事物能夠同時(simultaneously)發生。

最常見的并行計算工具是程序和線程。 程序和線程獨立執行,也可能同時執行:在不同的處理器上,甚至在不同的計算機上,但多個線程可以共享單個程序的記憶體。

相比之下,事件循環将其工作分解為任務并串行執行,不允許并行通路和更改共享記憶體。 并行和串行可以在不同線程中以協作事件循環的形式共存。

并行執行線程的交織和異步事件的交織發生在非常不同的粒度級别。

例如:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?

雖然 later() 的全部内容将被視為單個事件循環隊列條目,但在考慮運作此代碼的線程時,實際上可能有十幾種不同的低級操作。 例如,answer = answer * 2 需要首先加載 answer 的目前值,然後将 2 放在某處,然後執行乘法,然後取結果并将其存儲回 answer。

在單線程環境中,線程隊列中的項是低級操作真的沒有關系,因為沒有什麼可以中斷線程。 但是如果你有一個并行系統,其中兩個不同的線程在同一個程式中運作,你很可能會出現不可預測的行為。

考慮下面這段代碼:

JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?
JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?
JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?
JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?
JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?
JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?
JavaScript 異步執行的學習筆記 - 什麼是事件循環 Event loop?