JS的事件輪詢(Event Loop)機制

前言

JS單線程、JS的事件循環(Event Loop)、執行棧、任務隊列(消息隊列)、主線程、宏隊列(macrotask)、微隊列(microtask),前端er相信不少人對這些詞並不陌生,即使對js的api熟能生巧,可是卻並不理解這些機制流程的話,那可能JS的提高很難了,這裏也是屬於提高JS的一個分水嶺,在介紹這些概念以前,咱們先思考幾個很是經典的面試題,答案最後公佈,看完這篇文章,或許就可以煥然大悟:透過現象看本質! 注:本章全部環境都是基於瀏覽器環境,暫不考慮node環境; 題目一:javascript

setTimeout(() => {
    console.log(1);
}, 0);

new Promise((resolve) => {
    console.log(2);
    resolve();
}).then(() => {
    console.log(3);
});

console.log(4);
// 輸出最後的結果
複製代碼

題目二:前端

setTimeout(() => {
    console.log(1);
}, 0);

new Promise((resolve) => {
    console.log(2);
    setTimeout(() => {
        console.log(5);
    }, 0);
    resolve();
}).then(() => {
    console.log(3);
});

console.log(4);
// 輸出最後的結果
複製代碼

題目三:java

setTimeout(() => {
    console.log(1);
}, 0);
new Promise((resolve,reject) =>{
    console.log(2)
    resolve(3)
}).then((val) =>{
    console.log(val);
})
console.log(4);
// 輸出最後的結果
複製代碼

題目四:node

let a = () => {
  setTimeout(() => {
    console.log('任務隊列函數1')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('a的for循環')
  }
  console.log('a事件執行完')
}

let b = () => {
  setTimeout(() => {
    console.log('任務隊列函數2')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('b的for循環')
  }
  console.log('b事件執行完')
}

let c = () => {
  setTimeout(() => {
    console.log('任務隊列函數3')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('c的for循環')
  }
  console.log('c事件執行完')
}

a();
b();
c();
// 輸出最後的結果
複製代碼

JS單線程

JavaScript爲何是單線程,難道不能實現爲多線程嗎?

進程與任務

通常狀況下,一個進程一次只能執行一個任務,若是有不少任務須要執行,不外乎三種解決方法:面試

(1)排隊:由於一個進程一次只能執行一個任務,只好等前面的任務執行完了,再執行後面的任務。 (2)新建進程:使用fork命令,爲每一個任務新建一個進程。 (3)新建線程:由於進程太耗費資源,因此現在的程序每每容許一個進程包含多個線程,由線程去完成任務。 它是一種單線程語言,全部任務都在一個線程上完成,即採用上面的第一種方法。一旦遇到大量任務或者遇到一個耗時的任務,網頁就會出現"假死",由於JavaScript停不下來,也就沒法響應用戶的行爲。ajax

單線程

JavaScript從誕生起就是單線程,這跟歷史有關係。緣由大概是不想讓瀏覽器變得太複雜,由於多線程須要共享資源、且有可能修改彼此的運行結果,對於一種網頁腳本語言來講,這就太複雜了。後來就約定俗成,JavaScript爲一種單線程語言。(Worker API能夠實現多線程,可是JavaScript自己始終是單線程的。)api

若是某個任務很耗時,好比涉及不少I/O(輸入/輸出)操做,那麼線程的運行大概是下面的樣子。 promise

JS單線程
上圖的綠色部分是程序的運行時間,紅色部分是等待時間。能夠看到,因爲I/O操做很慢,因此這個線程的大部分運行時間都在空等I/O操做的返回結果。這種運行方式稱爲"同步模式"(synchronous I/O)或"堵塞模式"(blocking I/O)。

若是採用多線程,同時運行多個任務,那極可能就是下面這樣。 瀏覽器

JS單線程

上圖代表,多線程不只佔用多倍的系統資源,也閒置多倍的資源,這顯然不合理。多線程

其實JavaScript單線程是指瀏覽器在解釋和執行javascript代碼時只有一個線程,即JS引擎線程,瀏覽器自身還會提供其餘線程來支持這些異步方法,瀏覽器的渲染線程大概有一下幾種:

JS引擎線程 事件觸發線程 定時觸發器線程 異步http請求線程 GUI渲染線程 ...

瀏覽器環境

js做爲主要運行在瀏覽器的腳本語言,js主要用途之一是操做DOM。 在js高程中舉過一個栗子,若是js同時有兩個線程,同時對同一個dom進行操做,這時瀏覽器應該聽哪一個線程的,如何判斷優先級? 爲了不這種問題,js必須是一門單線程語言,而且在將來這個特色也不會改變。

解決的問題

Event Loop就是爲了解決這個問題而提出的。

"Event Loop是一個程序結構,用於等待和發送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"

簡單說,就是在程序中設置兩個線程:一個負責程序自己的運行,稱爲"主線程";另外一個負責主線程與其餘進程(主要是各類I/O操做)的通訊,被稱爲"Event Loop線程"(能夠譯爲"消息線程")。

JS單線程
上圖主線程的綠色部分,仍是表示運行時間,而橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,而後接着日後運行,因此不存在紅色的等待時間。等到I/O程序完成操做,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。

能夠看到,因爲多出了橙色的空閒時間,因此主線程得以運行更多的任務,這就提升了效率。這種運行方式稱爲"異步模式"(asynchronous I/O)或"非堵塞模式"(non-blocking mode)。

這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構成了很大的限制,但也所以使它具有了其餘語言不具有的優點。若是部署得好,JavaScript程序是不會出現堵塞的,這就是爲何node.js平臺能夠用不多的資源,應付大流量訪問的緣由。

執行棧與任務隊列

由於js是單線程語言,當遇到異步任務(如ajax操做等)時,不可能一直等待異步完成,再繼續往下執行,在這期間瀏覽器是空閒狀態,顯而易見這會致使巨大的資源浪費。

執行棧

當執行某個函數、用戶點擊一次鼠標,Ajax完成,一個圖片加載完成等事件發生時,只要指定過回調函數,這些事件發生時就會進入任務隊列中,等待主線程讀取,遵循先進先出原則。

執行任務隊列中的某個任務,這個被執行的任務就稱爲執行棧。

主線程

要明確的一點是,主線程跟執行棧是不一樣概念,主線程規定如今執行執行棧中的哪一個事件。 **主線程循環:**即主線程會不停的從執行棧中讀取事件,會執行完全部棧中的同步代碼。 當遇到一個異步事件後,並不會一直等待異步事件返回結果,而是會將這個事件掛在與執行棧不一樣的隊列中,咱們稱之爲任務隊列(Task Queue)。 當主線程將執行棧中全部的代碼執行完以後,主線程將會去查看任務隊列是否有任務。若是有,那麼主線程會依次執行那些任務隊列中的回調函數。

js異步執行的運行機制

1)全部任務都在主線程上執行,造成一個執行棧。 2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。 3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列"。那些對應的異步任務,結束等待狀態,進入執行棧並開始執行。

主線程不斷重複上面的第三步。

瀏覽器事件機制

瀏覽器在執行js代碼過程當中會維護一個執行棧,每一個方法都會進棧執行以後而後出棧(FIFO)。與此同時,瀏覽器又維護了一個消息隊列,全部的異步方法,在執行結束後都會將回調方法塞入消息隊列中,當全部執行棧中的任務所有執行完畢後,瀏覽器開始往消息隊列尋找任務,先進入消息隊列的任務先執行。

瀏覽器事件機制

宏任務和微任務

那麼若是兩個不一樣種類的異步任務執行後,哪一個會先執行?就像開頭提到的面試題,setTimeout和promise哪一個會先執行?這時候要提到概念:宏任務和微任務。 概念以下:

**宏任務(Macrotasks):**js同步執行的代碼塊,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等。 **微任務(Microtasks):**promise、process.nextTick(node環境)、Object.observe, MutationObserver等。

執行棧中執行的任務都是宏任務,當宏任務遇到Promise的時候會建立微任務,當Promise狀態fullfill的時候塞入微任務隊列。在一次宏任務完成後,會檢查微任務隊列有沒有須要執行的任務,有的話按順序執行微任務隊列中全部的任務。以後再開始執行下一次宏任務。具體步驟:

(1)執行主代碼塊 (2)若遇到Promise,把then以後的內容放進微任務隊列 (3)一次宏任務執行完成,檢查微任務隊列有無任務 (4)有的話執行全部微任務 (5)執行完畢後,開始下一次宏任務。

如何區分宏任務和微任務呢?劃分的標準是什麼?

宏任務本質:參與了事件循環的任務。

回到 Chromium 中,須要處理的消息主要分紅了三類:

Chromium 自定義消息 Socket 或者文件等 IO 消息 UI 相關的消息

  1. 與平臺無關的消息,例如 setTimeout 的定時器就是屬於這個
  2. Chromium 的 IO 操做是基於 libevent 實現,它自己也是一個事件驅動的庫
  3. UI 相關的其實屬於 blink 渲染引擎過來的消息,例如各類 DOM 的事件 其實與 JavaScript 的引擎無關,都是在 Chromium 實現的。

微任務本質:直接在 Javascript 引擎中的執行的,沒有參與事件循環的任務。

(1)是個內存回收的清理任務,使用過 Java 的童鞋應該都很熟悉,只是在 JavaScript 這是V8內部調用的 (2)就是普通的回調,MutationObserver 也是這一類 (3)Callable (4)包括 Fullfiled 和 Rejected 也就是 Promise 的完成和失敗 (5)Thenable 對象的處理任務

宏任務,微任務的優先級

promise是在當前腳本代碼執行完後,馬上執行的,它並無參與事件循環,因此它的優先級是高於 setTimeout。 宏任務和微任務的總結: 宏任務 Macrotasks 就是參與了事件循環的異步任務。 微任務 Microtasks 就是沒有參與事件循環的「異步」任務。

執行順序

一、先執行主線程 二、遇到宏隊列(macrotask)放到宏隊列(macrotask) 三、遇到微隊列(microtask)放到微隊列(microtask) 四、主線程執行完畢 五、執行微隊列(microtask),微隊列(microtask)執行完畢 六、執行一次宏隊列(macrotask)中的一個任務,執行完畢 七、執行微隊列(microtask),執行完畢 八、依次循環。。。

Event Loop(事件循環)

  js是單線程的,執行較長的js時候,頁面會卡死,沒法響應,可是全部的操做都會被記住到另外的隊列。好比:點擊了一個元素,不會馬上的執行,可是等到js加載完畢後就會執行剛纔點擊的操做,可以知道有一個隊列記錄了全部有待執行的操做,這個隊列分爲微觀和宏觀。微觀會比宏觀執行得更快。

  event loop它最主要是分三部分:主線程、宏隊列(macrotask)、微隊列(microtask) js的任務隊列分爲同步任務和異步任務,全部的同步任務都是在主線程裏執行的,異步任務可能會在macrotask或者microtask裏面。

  事件循環就是多線程的一種工做方式,Chrome裏面是使用了共享的task_runner對象給本身和其它線程post task過來存起來,用一個死循環不斷地取出task執行,或者進入休眠等待被喚醒。Mac的Chrome渲染線程和瀏覽器線程還藉助了Mac的sdk Cococa的NSRunLoop來作爲UI事件的消息源。Chrome的多進程通訊(不一樣進程的IO線程的本地socket通訊)藉助了libevent的事件循環,並加入了到了主消息循環裏面。

稱爲事件循環的緣由大多來源於源碼:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}
複製代碼

宏任務 > 全部微任務 > 宏任務,以下圖所示:

事件循環

事件循環中,每一次循環稱爲 tick, 每一次tick的任務以下:

執行棧選擇最早進入隊列的宏任務(一般是script總體代碼),若是有則執行 檢查是否存在 Microtask,若是存在則不停的執行,直至清空 microtask 隊列 更新render(每一次事件循環,瀏覽器均可能會去更新渲染) 重複以上步驟

Event Loop總體流程

事件循環

題目解析

題目一:

答案:2 4 3 1
複製代碼

(1)setTimeout丟給瀏覽器的異步線程處理,由於時間是0,立刻放入消息隊列 (2)new Promise裏面的console.log(2)加入執行棧,並執行,而後退出 (3)直接resolve,then後面的內容加入微任務隊列 (4)console.log(4)加入執行棧,執行完成後退出 (5)檢查微任務隊列,發現有任務,執行console.log(3) (6)發現消息隊列有任務,執行下一次宏任務console.log(1)

題目二:

答案:2 4 3 1 5
複製代碼

(1)setTimeout丟給瀏覽器的異步線程處理,由於時間是0,立刻放入消息隊列 (2)new Promise裏面的console.log(2)加入執行棧,並執行 (3)setTimeout給瀏覽器的異步線程處理,由於時間是0,立刻放入消息隊列,而後退出 (4)直接resolve,then後面的內容加入微任務隊列 (5)console.log(4)加入執行棧,執行完成後退出 (6)檢查微任務隊列,發現有任務,執行console.log(3) (7)發現消息隊列有任務,執行下一次宏任務console.log(1) (8)發現消息隊列有任務,執行下一次宏任務console.log(5)

題目三:

答案:2 4 3 1
複製代碼

(1)先執行script同步代碼: 先執行new Promise中的console.log(2),then後面的不執行屬於微任務而後執行console.log(4) (2)執行完script宏任務後,執行微任務,console.log(3),沒有其餘微任務了 (3)執行另外一個宏任務,定時器,console.log(1)

題目四:

答案:
(5000)a的for循環
a事件執行完
(5000)b的for循環
b事件執行完
(5000)c的for循環
c事件執行完
任務隊列函數1
任務隊列函數2
任務隊列函數3
複製代碼

結果是當a、b、c函數都執行完成以後,三個setTimeout纔會依次執行

node環境中的事件機制

node環境中的事件機制要比瀏覽器複雜不少,node的事件輪詢有階段的概念。每一個階段切換的時候執行,process.nextTick之類的全部微任務。

node環境中的事件機制

timer階段

執行全部的時間已經到達的計時事件

peding callbacks階段

這個階段將執行全部上一次poll階段沒有執行的I/O操做callback,通常是報錯。

idle.prepare

能夠忽略

poll階段

這個階段特別複雜

阻塞等到全部I/O操做,執行全部的callback. 全部I/O回調執行完,檢查是否有到時的timer,有的話回到timer階段 沒有timer的話,進入check階段.

check階段

執行setImmediate

close callbacks階段

執行全部close回調事件,例如socket斷開。

相關文章
相關標籤/搜索