搜尋
首頁web前端js教程JavaScript運作機制之事件循環(Event Loop)詳解_javascript技巧

一、為什麼JavaScript是單執行緒?

JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript不能有多個執行緒呢?這樣能提高效率啊。

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單線程,否則會帶來複雜的同步問題。例如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,而這時瀏覽器應該以哪個線程為準?
所以,為了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。

為了利用多核心CPU的運算能力,HTML5提出Web Worker標準,允許JavaScript腳本建立多個線程,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

二、任務隊列

單執行緒就意味著,所有任務都需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就得一直等著。

如果排隊是因為運算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒著的,因為IO設備(輸入輸出設備)很慢(例如Ajax操作從網路讀取資料) ,不得不等著結果出來,再往下執行。

JavaScript語言的設計者意識到,這時CPU完全可以不管IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備回傳了結果,再回過頭,把掛起的任務繼續執行下去。

於是,JavaScript就有了兩種執行方式:一種是CPU按順序執行,前一個任務結束,再執行下一個任務,這叫做同步執行;另一種是CPU跳過等待時間長的任務,先處理後面的任務,這叫做非同步執行。程式設計師自主選擇,採用哪一種執行方式。

具體來說,非同步執行的運作機制如下。 (同步執行也是如此,因為它可以被視為沒有非同步任務的非同步執行。)

(1)所有任務都在主執行緒上執行,形成一個執行堆疊(execution context stack)。
(2)主執行緒之外,還存在一個"任務隊列"(task queue)。系統把非同步任務放到"任務隊列"之中,然後繼續執行後續的任務。
(3)一旦"執行堆疊"中的所有任務執行完畢,系統就會讀取"任務佇列"。如果這個時候,非同步任務已經結束了等待狀態,就會從"任務佇列"進入執行棧,恢復執行。
(4)主執行緒不斷重複上面的第三步。

下圖就是主執行緒和任務佇列的示意圖。

只要主執行緒空了,就會去讀取"任務佇列",這就是JavaScript的運作機制。這個過程會不斷重複。

三、事件與回調函數

"任務隊列"實質上是一個事件的隊列(也可以理解成訊息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主執行緒讀取"任務佇列",就是讀取裡面有哪些事件。

"任務佇列"中的事件,除了IO設備的事件以外,還包括一些使用者產生的事件(例如滑鼠點擊、頁面滾動等等)。只要指定過回呼函數,這些事件發生時就會進入"任務佇列",等待主執行緒讀取。

所謂"回呼函數"(callback),就是那些會被主執行緒掛起來的程式碼。非同步任務必須指定回呼函數,當非同步任務從"任務佇列"回到執行棧,回呼函數就會執行。

"任務佇列"是先進先出的資料結構,排在前面的事件,優先返回主執行緒。主執行緒的讀取過程基本上是自動的,只要執行堆疊一清空,"任務佇列"上第一位的事件就會自動回到主執行緒。但是,由於存在後文提到的"定時器"功能,主執行緒要檢查執行時間,某些事件必須在規定的時間返回主執行緒。

四、Event Loop

主執行緒從"任務佇列"讀取事件,這個過程是循環不斷的,所以整個的這種運作機制又稱為Event Loop(事件循環)。

為了更能理解Event Loop,請看下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)。

上圖中,主執行緒運行的時候,產生堆疊(heap)和堆疊(stack),堆疊中的程式碼呼叫各種外部API,它們在"任務佇列"中加入各種事件(click,load, done)。只要堆疊中的程式碼執行完畢,主執行緒就會去讀取"任務佇列",依序執行那些事件所對應的回呼函數。

執行堆疊中的程式碼,總是在讀取"任務佇列"之前執行。請看下面這個例子。

複製程式碼 程式碼如下:

var req = new XMLHttpRequest();
    req.open('GET', url);   
    req.onload = function (){};   
    req.onerror = function (){};   
    req.send();

上面程式碼中的req.send方法是Ajax操作向伺服器發送數據,它是一個非同步任務,意味著只有當前腳本的所有程式碼執行完,系統才會去讀取"任務佇列"。所以,它與下面的寫法等價。
複製程式碼 程式碼如下:

  var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};   
    req.onerror = function (){};  

也就是說,指定回呼函數的部分(onload和onerror),在send()方法的前面或後面無關緊要,因為它們屬於執行棧的一部分,系統總是執行完它們,才會去讀取"任務隊列"。

五、定時器

除了放置非同步任務,"任務佇列"還有一個作用,就是可以放置定時事件,即指定某些程式碼在多少時間之後執行。這叫做"定時器"(timer)功能,也就是定時執行的程式碼。

定時器功能主要由setTimeout()和setInterval()這兩個函數來完成,它們的內部運作機製完全一樣,區別在於前者指定的程式碼是一次性執行,後者則為反覆執行。以下主要討論setTimeout()。

setTimeout()接受兩個參數,第一個是回呼函數,第二個是延遲執行的毫秒數。

複製程式碼 程式碼如下:

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面程式碼的執行結果是1,3,2,因為setTimeout()將第二行延後到1000毫秒之後執行。

如果將setTimeout()的第二個參數設為0,就表示目前程式碼執行完(執行堆疊清空)以後,立即執行(0毫秒間隔)指定的回呼函數。

複製程式碼 程式碼如下:

setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面程式碼的執行結果總是2,1,因為只有在執行完第二行以後,系統才會去執行"任務佇列"中的回呼函數。
HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,如果低於這個值,就會自動增加。在此之前,舊版的瀏覽器都將最短間隔設為10毫秒。

另外,對於那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常不會立即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果比setTimeout()好。

要注意的是,setTimeout()只是將事件插入了"任務佇列",必須等到目前程式碼(執行堆疊)執行完,主執行緒才會去執行它指定的回呼函數。要是當前程式碼耗時很長,有可能要等很久,所以沒有辦法保證,回呼函數一定會在setTimeout()指定的時間執行。

六、Node.js的Event Loop

Node.js也是單執行緒的Event Loop,但是它的運作機制不同於瀏覽器環境。

請看下面的示意圖(作者@BusyRich)。

根據上圖,Node.js的運作機制如下。

(1)V8引擎解析JavaScript腳本。
(2)解析後的程式碼,呼叫Node API。
(3)libuv函式庫負責Node API的執行。它將不同的任務分配給不同的線程,形成一個Event Loop(事件循環),以非同步的方式將任務的執行結果傳回V8引擎。
(4)V8引擎再將結果回傳給使用者。

除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與"任務隊列"有關的方法:process.nextTicksetImmediate。它們可以幫助我們加深對"任務隊列"的理解。

process.nextTick方法可以在目前"執行棧"的尾部----主執行緒下一次讀取"任務佇列"之前----觸發回調函數。也就是說,它指定的任務總是發生在所有非同步任務之前。 setImmediate方法則是在目前"任務佇列"的尾端觸發回呼函數,也就是說,它指定的任務總是在主執行緒下一次讀取"任務佇列"時執行,這與setTimeout(fn, 0)很像。請看下面的範例(via StackOverflow)。

複製程式碼 程式碼如下:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED


上面程式碼中,由於process.nextTick方法指定的回呼函數,總是在當前"執行棧"的尾部觸發,所以不僅函數A比setTimeout指定的回調函數timeout先執行,而且函數B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否嵌套),將全部在目前"執行棧"執行。

現在,再看setImmediate。

複製程式碼 程式碼如下:

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// TIMEOUT FIRED
// 2

上面程式碼中,有兩個setImmediate。第一個setImmediate,指定在當前"任務隊列"尾部(下一次"事件循環"時)觸發回調函數A;然後,setTimeout也是指定在當前"任務隊列"尾部觸發回調函數timeout,所以輸出結果中,TIMEOUT FIRED排在1的後面。至於2排在TIMEOUT FIRED的後面,是因為setImmediate的另一個重要特點:一次"事件循環"只能觸發一個由setImmediate指定的回呼函數。

我們因此得到了一個重要差異:多個process.nextTick語句總是一次執行完,多個setImmediate則需要多次才能執行完。事實上,這正是Node.js 10.0版加入setImmediate方法的原因,否則像下面這樣的遞歸呼叫process.nextTick,將會沒完沒了,主執行緒根本不會去讀取"事件佇列"!

複製程式碼 程式碼如下:

process.nextTick(function foo() {
  process.nextTick(foo);
});

事實上,現在要是你寫出遞歸的process.nextTick,Node.js會拋出一個警告,要你改成setImmediate。
另外,由於process.nextTick指定的回呼函數是在本次"事件循環"觸發,而setImmediate指定的是在下次"事件循環"觸發,所以很顯然,前者總是比後者發生得早,而且執行效率也高(因為不用檢查"任務隊列")。
陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
JavaScript數據類型:瀏覽器和nodejs之間是否有區別?JavaScript數據類型:瀏覽器和nodejs之間是否有區別?May 14, 2025 am 12:15 AM

JavaScript核心數據類型在瀏覽器和Node.js中一致,但處理方式和額外類型有所不同。 1)全局對像在瀏覽器中為window,在Node.js中為global。 2)Node.js獨有Buffer對象,用於處理二進制數據。 3)性能和時間處理在兩者間也有差異,需根據環境調整代碼。

JavaScript評論:使用//和 / * * / * / * /JavaScript評論:使用//和 / * * / * / * /May 13, 2025 pm 03:49 PM

JavaScriptusestwotypesofcomments:single-line(//)andmulti-line(//).1)Use//forquicknotesorsingle-lineexplanations.2)Use//forlongerexplanationsorcommentingoutblocksofcode.Commentsshouldexplainthe'why',notthe'what',andbeplacedabovetherelevantcodeforclari

Python vs. JavaScript:開發人員的比較分析Python vs. JavaScript:開發人員的比較分析May 09, 2025 am 12:22 AM

Python和JavaScript的主要區別在於類型系統和應用場景。 1.Python使用動態類型,適合科學計算和數據分析。 2.JavaScript採用弱類型,廣泛用於前端和全棧開發。兩者在異步編程和性能優化上各有優勢,選擇時應根據項目需求決定。

Python vs. JavaScript:選擇合適的工具Python vs. JavaScript:選擇合適的工具May 08, 2025 am 12:10 AM

選擇Python還是JavaScript取決於項目類型:1)數據科學和自動化任務選擇Python;2)前端和全棧開發選擇JavaScript。 Python因其在數據處理和自動化方面的強大庫而備受青睞,而JavaScript則因其在網頁交互和全棧開發中的優勢而不可或缺。

Python和JavaScript:了解每個的優勢Python和JavaScript:了解每個的優勢May 06, 2025 am 12:15 AM

Python和JavaScript各有優勢,選擇取決於項目需求和個人偏好。 1.Python易學,語法簡潔,適用於數據科學和後端開發,但執行速度較慢。 2.JavaScript在前端開發中無處不在,異步編程能力強,Node.js使其適用於全棧開發,但語法可能複雜且易出錯。

JavaScript的核心:它是在C還是C上構建的?JavaScript的核心:它是在C還是C上構建的?May 05, 2025 am 12:07 AM

javascriptisnotbuiltoncorc; sanInterpretedlanguagethatrunsonenginesoftenwritteninc.1)JavascriptwasdesignedAsignedAsalightWeight,drackendedlanguageforwebbrowsers.2)Enginesevolvedfromsimpleterterpretpretpretpretpreterterpretpretpretpretpretpretpretpretpretcompilerers,典型地,替代品。

JavaScript應用程序:從前端到後端JavaScript應用程序:從前端到後端May 04, 2025 am 12:12 AM

JavaScript可用於前端和後端開發。前端通過DOM操作增強用戶體驗,後端通過Node.js處理服務器任務。 1.前端示例:改變網頁文本內容。 2.後端示例:創建Node.js服務器。

Python vs. JavaScript:您應該學到哪種語言?Python vs. JavaScript:您應該學到哪種語言?May 03, 2025 am 12:10 AM

選擇Python還是JavaScript應基於職業發展、學習曲線和生態系統:1)職業發展:Python適合數據科學和後端開發,JavaScript適合前端和全棧開發。 2)學習曲線:Python語法簡潔,適合初學者;JavaScript語法靈活。 3)生態系統:Python有豐富的科學計算庫,JavaScript有強大的前端框架。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具