javascript
JavaScript 异步执行的学习笔记 - 什么是事件循环 Event loop?
原文
使用像 JavaScript 這樣的語言進行編程時,最重要但也經常被誤解的部分之一是如何表達和操作一段需要某段時間才能完成執行的程序行為。
這不僅僅是從 for 循環開始到 for 循環結束發生的事情,這當然需要一些時間(微秒到毫秒)才能完成。它是關于當你的程序的一部分現在運行而你的程序的另一部分稍后運行時會發生什么。在程序的兩部分分別得到執行的時間間隙,存在著一個 gap.
實際上,所有編寫過的重要程序(尤其是用 JS 編寫的)都必須以某種方式管理這個 gap,無論是等待用戶輸入、從數據庫或文件系統請求數據、通過網絡發送數據以及等待響應,或以固定的時間間隔執行重復的任務(如動畫)。通過所有這些不同的方式,您的程序必須及時管理狀態。
異步編程從 JS 開始就已經存在了,這是肯定的。但是大多數 JS 開發人員從來沒有真正仔細考慮過它是如何以及為什么會出現在他們的程序中,或者探索各種其他方法來處理它。足夠好的方法一直是不起眼的回調函數。直到今天,許多人仍堅持認為回調已綽綽有余。
但是隨著 JS 的范圍和復雜性不斷增長,為了滿足在瀏覽器和服務器以及介于兩者之間的所有可能的設備中運行的一流編程語言不斷擴大的需求,我們管理異步的痛苦正變得越來越嚴重,他們迫切需要更有能力和更合理的方法。
我們必須更深入地了解異步是什么以及它如何在 JS 中運行。
a program in chunks
你可以在一個 .js 文件中編寫你的 JS 程序,但你的程序幾乎肯定由幾個塊組成,其中只有一個現在要執行,其余的將稍后執行。 每個塊最常見的單位是函數。
大多數剛接觸 JS 的開發人員似乎都有的問題是,“later”不會嚴格地發生在“now”之后。 換句話說,根據定義,當前無法完成的任務將異步完成,因此我們不會像您直觀地期望或想要的那樣有阻塞行為。
考慮:
// ajax(..) is some arbitrary Ajax function given by a library var data = ajax( "http://some.url.1" );console.log( data ); // Oops! `data` generally won't have the Ajax results您可能知道標準 Ajax 請求不是同步完成的,這意味著 ajax(…) 函數還沒有任何返回值以分配給 data 變量。 如果 ajax(…) 可以阻塞直到響應回來,那么 data = … 賦值會正常工作。
但這不是我們使用 Ajax 的方式。 我們現在發出一個異步的 Ajax 請求,直到稍后我們才會得到結果。
從現在到以后“等待”的最簡單(但絕對不僅僅是,甚至最好!)方法是使用一個函數,通常稱為回調函數:
// ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", function myCallbackFunction(data){console.log( data ); // Yay, I gots me some `data`!} );看這段代碼:
function now() {return 21; }function later() {answer = answer * 2;console.log( "Meaning of life:", answer ); }var answer = now();setTimeout( later, 1000 ); // Meaning of life: 42這個程序有兩個部分:現在將運行的內容和稍后運行的內容。 這兩個塊是什么應該很明顯。
現在立即執行的代碼塊:
function now() {return 21; }function later() { .. }var answer = now();setTimeout( later, 1000 );稍后異步執行的代碼塊:
answer = answer * 2; console.log( "Meaning of life:", answer );一旦您執行程序, now 塊就會立即運行。 但是 setTimeout(…) 也會設置一個事件(超時)稍后發生,因此 later() 函數的內容將在稍后的時間(從現在起 1,000 毫秒)執行。
任何時候您將一部分代碼包裝到一個函數中并指定它應該響應某些事件(計時器、鼠標單擊、Ajax 響應等)而執行時,您正在創建代碼的“later”部分,從而引入異步到你的程序。
Async Console
console.log 到底是同步輸出還是異步輸出?
沒有關于 console.* 方法如何工作的規范或一組要求——它們不是 JavaScript 的正式組成部分,而是由托管環境添加到 JS 中。
因此,不同的瀏覽器和 JS 環境有著各自的實現,這有時會導致混亂的行為。
特別是,有一些瀏覽器和一些條件,console.log(…) 實際上并沒有立即輸出它給出的內容。 這可能發生的主要原因是因為 I/O 是許多程序(不僅僅是 JS)的一個非常緩慢和阻塞的部分。 因此,瀏覽器在后臺異步處理控制臺 I/O 可能會表現得更好(從頁面/UI 角度來看),而您甚至可能不知道發生了這種情況。
一個不太常見但可能的場景,可以觀察到這種情況(不是從代碼本身而是從外部):
var a = {index: 1 };// later console.log( a ); // ??// even later a.index++;我們通常希望在 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)的邏輯可以用下面的偽代碼來表示:
// `eventLoop` is an array that acts as a queue // (first-in, first-out) var eventLoop = [ ]; var event;// keep going "forever" while (true) {// perform a "tick"if (eventLoop.length > 0) {// get the next event in the queueevent = eventLoop.shift();// now, execute the next eventtry {event();}catch (err) {reportError(err);}} }當然,這是為了說明概念而大大簡化的偽代碼。但這應該足以幫助獲得更好的理解。
如您所見,while 循環代表了一個持續運行的循環,該循環的每次迭代稱為一個滴答。對于每個滴答聲,如果一個事件在隊列中等待,它就會被從隊列里摘下并執行。這些事件是您的函數回調。
重要的是要注意 setTimeout(…) 不會將您的回調放在事件循環隊列中。它的作用是設置一個計時器;當計時器到期時,環境會將您的回調放入事件循環中,以便將來某個滴答聲將其拾取并執行。
如果此時事件循環中已經有 20 個項目怎么辦?您的回調等待。它排在其他人后面——通常沒有用于搶占隊列和跳過隊列的路徑。這解釋了為什么 setTimeout(…) 計時器可能無法以完美的時間精度觸發。您可以保證(粗略地說)您的回調不會在您指定的時間間隔之前觸發,但它可以在該時間或之后發生,具體取決于事件隊列的狀態。
因此,換句話說,您的程序通常被分解成許多小塊,這些小塊在事件循環隊列中一個接一個地發生。從技術上講,與您的程序不直接相關的其他事件也可以在隊列中交錯。
Parallel Threading
將術語“異步 async”和“并行 parallel”混為一談是很常見的,但它們實際上是完全不同的。 請記住,異步是關于現在和以后之間的gap. 但并行是指事物能夠同時(simultaneously)發生。
最常見的并行計算工具是進程和線程。 進程和線程獨立執行,也可能同時執行:在不同的處理器上,甚至在不同的計算機上,但多個線程可以共享單個進程的內存。
相比之下,事件循環將其工作分解為任務并串行執行,不允許并行訪問和更改共享內存。 并行和串行可以在不同線程中以協作事件循環的形式共存。
并行執行線程的交織和異步事件的交織發生在非常不同的粒度級別。
例如:
function later() {answer = answer * 2;console.log( "Meaning of life:", answer ); }雖然 later() 的全部內容將被視為單個事件循環隊列條目,但在考慮運行此代碼的線程時,實際上可能有十幾種不同的低級操作。 例如,answer = answer * 2 需要首先加載 answer 的當前值,然后將 2 放在某處,然后執行乘法,然后取結果并將其存儲回 answer。
在單線程環境中,線程隊列中的項是低級操作真的沒有關系,因為沒有什么可以中斷線程。 但是如果你有一個并行系統,其中兩個不同的線程在同一個程序中運行,你很可能會出現不可預測的行為。
考慮下面這段代碼:
var a = 20;function foo() {a = a + 1; }function bar() {a = a * 2; }// ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar );總結
以上是生活随笔為你收集整理的JavaScript 异步执行的学习笔记 - 什么是事件循环 Event loop?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《霍格沃茨之遗》游戏累计售出 1500
- 下一篇: 不使用任何框架,手写纯 JavaScri