js Event Loop 運行機制

Event Loop,事件環,線程進程。這些概念對初識前端的同窗來講可能會一頭霧水。並且運行js代碼的運行環境除了瀏覽器還有node。所以不一樣環境處理Event Loop又變得不一樣,十分容易混淆。若是你有這樣的疑問。下文將給你一個清晰的解釋。javascript

概念梳理

首先咱們簡化一下概念,把進程,線程,事件環,這些概念梳理一下。清晰了概念後面用到的時候就會有共鳴。html

進程和線程基本概念

拿出在教科書裏的概念:前端

一、調度:線程做爲調度和分配的基本單位,進程做爲擁有資源的基本單位;java

二、併發性:不只進程之間能夠併發執行,同一個進程的多個線程之間也可併發執行;node

三、擁有資源:進程是擁有資源的一個獨立單位,線程不擁有系統資源,但能夠訪問隸屬於進程的資源;git

四、系統開銷:在建立或撤消進程時,因爲系統都要爲之分配和回收資源,致使系統的開銷明顯大於建立或撤消線程時的開銷。es6

進程和線程的關係:github

  1. 一個線程只能屬於一個進程,而一個進程能夠有多個線程,但至少有一個線程;
  2. 資源分配給進程,同一進程的全部線程共享該進程的全部資源;
  3. 處理機分給線程,即真正在處理機上運行的是線程;
  4. 線程在執行過程當中,須要協做同步。不一樣進程的線程間要利用消息通訊的辦法實現同步。線程是指進程內的一個執行單元,也是進程內的可調度實體。
    第一次看可能並沒什麼共鳴。可是帶着最基本的想法,一個進程能夠有多個線程,線程之間能夠相互通訊。這兩點,就足夠你理解後續事件環的知識。

瀏覽器中的進程和線程和Event Loop

瀏覽器的進程

  1. 從打開瀏覽器開始,打開瀏覽器,咱們首先看到的是,用戶界面,這裏有搜索框,顯示區,還有收藏夾等等。這些會分配一個進程。
  2. 咱們看到瀏覽器本身會實現一些本地存儲,cookie等,這些操做也須要分配一個進程。

3. 打開一個瀏覽器的tab頁,他若是想要運行就須要系統分配給他cpu和內存資源,所以他須要分配一個進程。對應上面的概念「進程是擁有資源的基本單位」。所以每打開一個tab就對應一個新的進程。從資源管理器中進程能夠看到,chrome佔用多個進程。(有些系統會對進程進行整合,win10下可能看到的效果不一樣)

眼見爲實,咱們才能說瀏覽器是多線程的。那咱們用可視化的角度看一下瀏覽器的這個進程和線程結構

從圖中看黃色的圓角框裏包裹的都是進程。藍色的直角框裏包裹的都是瀏覽器渲染引擎(瀏覽器內核)所包含的線程。對應上面的概念「一個進程能夠有多個線程,但至少有一個線程」。前三個進程剛剛在1-3裏都說過了。 介紹了剛剛那麼多咱們前端的操做其實都是在3中瀏覽器渲染引擎中處理。真正幹活的就是線程。對應上面的概念「處理機分給線程,即真正在處理機上運行的是線程」。

瀏覽器內核的線程

接下來看一下瀏覽器引擎(進程)中包含哪些線程chrome

  • UI渲染線程 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行

注意:UI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),UI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。後端

  • js引擎線程(JS解析線程) 也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎) JS引擎線程負責解析Javascript腳本,運行代碼。 JS引擎一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(renderer進程)中不管何時都__只有一個JS線程在運行JS程序__

一樣注意:UI渲染線程與JS引擎線程是互斥的,因此若是JS執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。

  • 事件觸發線程 __歸屬於瀏覽器__而不是JS引擎,用來控制事件循環(能夠理解,JS引擎本身都忙不過來,須要瀏覽器另開線程協助) 當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其餘線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理

注意:因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)

  • 定時觸發器線程 傳說中的setInterval與setTimeout所在線程 瀏覽器定時計數器並非由JavaScript引擎計數的,(由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確) 所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)

注意:W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。

  • 異步http請求線程 在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

js渲染引擎的Event Loop

以上線程,每一個拿出來均可以詳細的說上一篇。Event Loop涉及到的JS引擎的一些運行機制的分析。咱們能夠將這些線程理解爲,

  • 一個主進程就是js引擎,其餘均爲輔助的線程。
  • 主進程存在一個執行棧,事件觸發線程維護一個消息隊列
  • 同步任務在執行棧中執行,異步任務在知足條件後加入到消息隊列中,等待執行。
  • 先執行棧中的任務,執行完畢後,檢查隊列是否爲空,不爲空,將隊列中的任務壓入執行棧中執行。直到棧和隊列均爲空。 js渲染引擎的Event Loop以下圖

這時候拿出幾道題看一下會更清晰 題目1:

setTimeout(function(){
    console.log(0)
},500)
setTimeout(function(){
    console.log(1)
},1000)
setTimeout(function(){
    console.log(2)
},2000)

for(;;){

}
複製代碼

上面這段代碼用於不會有輸出,同步代碼死循環阻塞了執行棧。雖然定時後回調加入執行隊列,可是異永遠不會執行。
題目二:

setTimeout(function(){
    console.log('setTimeout1');
    Promise.resolve().then(()=>{
        console.log('then1');
    });
},0)
Promise.resolve().then(()=>{
    console.log('then2');
    Promise.resolve().then(()=>{
        console.log('then3');
    })
    setTimeout(function(){
        console.log('setTimeout2');
    },0)
})
複製代碼

答案:then2 then3 setTimeout1 then1 setTimeout2
首先在題目中出現了es6的promise,他的出現讓原來咱們理解的__事件環產生了一些不一樣__。 爲何呢?由於Promise裏有了一個一個新的概念:microtask 此時JS中分爲__兩種任務類型__:macrotask和microtask,在ECMAScript中,microtask稱爲jobs,macrotask可稱爲task

微任務和宏任務

首先說明,是以__瀏覽器爲處理環境__下的執行邏輯 瀏覽器環境下的微任務和宏任務有哪些 宏任務:setTimeout setImmediate MessageChannel 微任務:Promise.then MutationObserver
記住兩點:

  • 微任務在宏任務以前的執行,先執行 執行棧中的內容 執行後 清空微任務
  • 每次取一個宏任務 就去清空微任務,以後再去取宏任務

而後題目入手分析宏任務和微任務的執行

  • setTimeout1放入宏任務執行隊列中,微任務then2放入微任務隊列中,棧爲空,優先執行微任務,則先執行then2。
  • then2以後執行後,接下來存在微任務then3。將then3放入微任務隊列中。
  • 接下來setTimeout2加入到宏任務隊列中。
  • 此時執行棧爲空,執行then3。
  • 微任務所有執行完畢後,執行宏任務setTimeout1,執行發現微任務then1,放置到微任務隊列中。
  • setTimeout1宏任務執行完,再次清空微任務隊列,執行then1
  • 微任務所有執行完畢後,執行宏任務setTimeout2。程序結束。

node運行環境中的進程和線程

Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。他的目標就是解析js代碼,讓他能運行起來。

node js 是單線程的

和瀏覽器環境下相似,他有一個解析js的主線程,其餘線程做爲輔助,可是由於不涉及操做dom,ui線程就不存在了。(各個線程的概念參考瀏覽器環境下的線程)
單線程在瀏覽器運行環境中的弊端體如今阻塞頁面執行。
那麼node做爲後端服務,單線程有什麼利弊?
優勢:

  1. 避免頻繁建立、切換進程的開銷,使執行速度更加迅速。
  2. 資源佔用小
  3. 線程安全,不用擔憂同一變量同時被多個線程進行讀寫而形成的程序崩潰。 缺點:
  4. 不適合大量的計算和壓縮等cpu密集型的操做,會形成阻塞。

node下Event Loop

事件環的總體仍是不變的,執行棧,消息隊列,api。不一樣的是,node下的消息隊列有所不一樣

分析一下node下的消息隊列

  • 爲微任務,定時器,io,setImmidiate分別分配消息隊列
  • 先檢查定時器隊列,若是有內容,則所有清空
  • 從時間隊列切換到io隊列的過程當中,檢查微任務,若是有則狀況微任務。
  • io隊列執行完成,若是有check隊列的內容,則執行。不然繼續檢查定時器隊列。
  • 完成閉環

從一個題目入手感覺一下node環境和瀏覽器環境下的不一樣

setTimeout(() => {
    console.log('timeout1');
    Promise.resolve().then(() => {
        console.log('promise');
    });

}, 0)
setTimeout(() => {
    console.log('timeout2');
}, 0)
複製代碼

瀏覽器下的結果:timeout1 promise timeout2
node下的結果:timout1 timeout2 promise

微任務和宏任務

node環境下的微任務和宏任務有哪些 宏任務:setTimeout setImmediate 微任務:Promise.then process.nextTick
題目三能夠很好的分析node環境下的任務執行 node環境下運行流程

  • 首先遇到兩個宏任務,均放入到時間隊列裏。
  • 執行時間隊列裏第一個宏任務時timeout1,遇到微任務promise,放到微任務隊列中
  • 此時時間隊列還未清空,繼續執行完成全部時間隊列裏的任務。執行timout2
  • 在切換io隊列時檢查微任務,有則執行清空微任務。執行promise。
    瀏覽器環境下運行流程
  • 首先遇到兩個宏任務,均放入到宏任務隊列裏。
  • 執行時間隊列裏第一個宏任務時timeout1,遇到微任務promise,放到微任務隊列中
  • timout1執行完成檢查微任務,有內容則執行清空,執行promise。
  • 清空微任務後再執行宏任務。執行timeout2

注意:一樣是微任務,process.nextTick,優於promise.then先執行

Promise.resolve().then(() => {
    console.log('then')
})
process.nextTick(() => {
    console.log('nextTick')
});
//nextTick then
複製代碼

注意:一樣是宏任務,setTimeout和setImediate執行的前後順序是不肯定的,依賴於執行棧執行的速度。

setImmediate(function () {
    console.log('setImmediate')
});
setTimeout(function () {
    console.log('setTimeout')
}, 0); // ->4
複製代碼

可是在以下場景下是有固定輸出的

let fs = require('fs');
fs.readFile('./gitignore', function () { // io的下一個事件隊列是check階段
    setImmediate(function () {
        console.log('setImmediate')
    });
    setTimeout(function () {
        console.log('setTimeout')
    }, 0); // ->4
})

複製代碼

給個提示,讀文件是io操做,io執行以後首先要check,check以後或沒有check內容再去檢查定時隊列。 那麼結果就留給你們自行分析了。

總結

但願這篇文章能給初識js的你一個清晰的大框,也是梳理我本身的知識。可能我理解的也很粗淺,有錯誤的地方,但願你們幫忙指正。

參考文獻

  1. 從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理
  2. Node.js的線程和進程詳解
相關文章
相關標籤/搜索