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

JavaScript運作機制之事件循環(Event Loop)詳解_javascript技巧

May 16, 2016 pm 04:34 PM
event javascript loop 事件循環 運作機制

一、為什麼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

熱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

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

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1317
25
PHP教程
1268
29
C# 教程
1246
24
如何使用WebSocket和JavaScript實現線上語音辨識系統 如何使用WebSocket和JavaScript實現線上語音辨識系統 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術 WebSocket與JavaScript:實現即時監控系統的關鍵技術 Dec 17, 2023 pm 05:30 PM

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統 如何利用JavaScript和WebSocket實現即時線上點餐系統 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統 如何使用WebSocket和JavaScript實現線上預約系統 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript與WebSocket:打造高效率的即時天氣預報系統 JavaScript與WebSocket:打造高效率的即時天氣預報系統 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

簡易JavaScript教學:取得HTTP狀態碼的方法 簡易JavaScript教學:取得HTTP狀態碼的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

javascript如何使用insertBefore javascript如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

JavaScript與WebSocket:打造高效率的即時影像處理系統 JavaScript與WebSocket:打造高效率的即時影像處理系統 Dec 17, 2023 am 08:41 AM

JavaScript是一種廣泛應用於Web開發的程式語言,而WebSocket則是一種用於即時通訊的網路協定。結合二者的強大功能,我們可以打造一個高效率的即時影像處理系統。本文將介紹如何利用JavaScript和WebSocket來實作這個系統,並提供具體的程式碼範例。首先,我們需要明確指出即時影像處理系統的需求和目標。假設我們有一個攝影機設備,可以擷取即時的影像數

See all articles