事件循環機制EventLoop

在說EventLoop以前咱們先看一道題html

setTimeout(() => {
    console.log(111);
}, 1000);

while (true) {
    console.log(22);
}
複製代碼

console.log(111); 永遠都不會輸出,由於javaScript 是單線程html5

線程與進程

概念

咱們常常說JS是單線程執行的,指的是一個進程裏只有一個主線程,那到底什麼是線程?什麼是進程?java

官方的說法是: 進程是CPU資源分配的最小單位;線程是CPU調度的最小單位。 這兩句話並很差理解,咱們先來看張圖:node

231232001.png

  • 進程比如圖中的工廠,有單獨的專屬本身的工廠資源
  • 線程比如圖中的工人,多個工人在一個工廠中協做工做,工廠與工廠時1:n的關係。也就是說 一個進程由一個或者多個線程組成,線程是一個進程中代碼的不一樣執行路線
  • 工廠的空間是工人們共享的,這象徵 一個進程的內存空間是共享的,每一個線程均可用這些共享內存
  • 多個工廠之間獨立存在

多進程與多線程

  • 多進程:在同一個時間裏,同一個計算機系統中若是容許兩個或兩個以上的進程處於運行狀態。多進程帶來的好處是明顯的,好比你能夠聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進程之間絲絕不後悔相互干擾
  • 多線程:程序中包含多個執行流,即在一個程序中能夠同時運行多個不一樣的線程來執行不一樣的任務,也就是說容許單個程序建立多個併發執行的線程來完成各自的任務。

以 Chrome 瀏覽器中爲例,當你打開一個 Tab 頁時,其實就是建立了一個進程,一個進程中能夠有多個線程(下文會詳細介紹),好比渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是建立了一個線程,當請求結束後,該線程可能就會被銷燬ios

什麼是單線程?

主程序只有一個線程,即同一時間片斷內其只能執行單個任務。ajax

JS爲何選擇單線程?

JavaScript的主要用途是與用戶交互,以及操做DOM,若是一個線程是執行刪除操做,一個是修改操做,那麼就會出現問題。所以決定了它只能是單線程,不然會帶來不少複雜的同步問題。axios

單線程意味着什麼?

單線程就意味着,同一時間只能執行一個任務,全部任務都須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就須要一直等着。這就會致使IO操做(耗時但CPU閒置)時形成性能浪費的問題。promise

如何解決單線程帶來的性能問題

答案是異步 ,主線程徹底能夠無論IO操做,暫時掛起處於等待中的任務,先運行排在後面的任務。等到IO操做返回告終果,在回過頭,把掛起的任務繼續執行下去。因而,因此任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。瀏覽器

瀏覽器內核

簡單來講瀏覽器內核是經過取得頁面內容,整理信息(應用CSS),計算和組合最終輸出可視化的圖像結果,一般也被成爲渲染引擎。markdown

瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由一下常駐線程組成:

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

GUI渲染線程

  • 主要負責頁面的渲染,解析 HTML、CSS,構建 DOM 樹,佈局和繪製等。
  • 當界面須要重繪或者因爲某種操做引起迴流時,將執行該線程。
  • 該線程與 JS 引擎線程互斥,當執行 JS 引擎線程時,GUI 渲染會被掛起,當任務隊列空閒時,JS 引擎纔會去執行 GUI 渲染。

JS 引擎線程

  • 該線程固然是主要負責處理 JavaScript 腳本,執行代碼。
  • JS引擎一直等待着任務隊列中任務獲得來,而後加以處理,一個Tab頁中不管何時都要有一個JS線程在運行js程序。
  • 固然,該線程與 GUI 渲染線程互斥,當 JS 引擎線程執行 JavaScript 腳本時間過長,將致使頁面渲染的阻塞。

定時器觸發線程

  • 負責執行異步定時器一類的函數的線程,如: setTimeout,setInterval。
  • 主線程依次執行代碼時,遇到定時器,會將定時器交給該線程處理,當計數完畢後,事件觸發線程會將計數完畢後的事件加入到任務隊列的尾部,等待 JS 引擎線程執行。
  • 瀏覽器定時計數器並非由JavaScript引擎計數得(由於JavaScript引擎是單線程得,若是處於阻塞線程狀態就會影響計時得準確)
  • 所以經過單線程來計數並觸發定時

事件觸發線程

  • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環
  • 當JS引擎執行代碼塊,會將對應任務添加到事件線程中
  • 對應得事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列得隊尾,等待JS引擎得處理
  • 注意:因爲JS得單線程關係,因此這些待處理隊列中得事件都得排隊等待JS引擎處理

異步 http 請求線程

  • 負責執行異步請求一類的函數的線程,如: Promise,axios,ajax 等。
  • 主線程依次執行代碼時,遇到異步請求,會將函數交給該線程處理,當監聽到狀態碼變動,若是有回調函數,事件觸發線程會將回調函數加入到任務隊列的尾部,等待 JS 引擎線程執行。

瀏覽器中的Event Loop

Micro-Task與Macro-Task

事件循環中的異步隊列有兩種:macro(宏任務)隊列和micro(微任務)隊列。宏任務隊列能夠有多個,微任務隊列只有一個

  • 常見的macro-task 好比:setTimeout、setInterval、 setImmediate、script(總體代碼)、 I/O 操做、UI 渲染等。
  • 常見的micro-task 好比: process.nextTick、new Promise().then(回調)、MutationObserver(html5 新特性) 等

Event Loop過程解析

一個完整的Event Loop過程,能夠歸納爲如下階段:

2019-01-14-002.png

  • 一開始執行棧空,咱們能夠把 執行棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。micro隊列空,macro隊列裏有且只有script腳本(總體代碼)。

  • 全局上下文(script標籤)被推入執行棧,同步代碼執行。在執行的過程當中,會判斷是同步任務仍是異步任務,經過對一些接口的調用,能夠產生新的macro-task與micro-task,它們會分別被推入各自的任務隊列裏。同步代碼執行完了,script腳本會被移出macro隊列,這個過程本質上是隊列的macro-task的執行和出隊的過程。

  • 上一步咱們出隊的是一個macro-task,這一步咱們處理的是micro-task。但須要注意的是:當macro-task出隊時,任務是一個一個執行的;而micro-task出隊時,任務時一隊一隊執行的。所以,咱們處理micro隊列這一步,會逐個執行隊列中的任務並把它出隊,知道隊列被清空。

  • 執行渲染操做,更新界面

  • 檢查是否存在Web worker任務,若是有,則對其進行處理

  • 上述過程循環往復,知道兩個隊列都清空

咱們總結一下,每次循環都是一個這樣的過程:

2019-01-14-003.png

當某個宏任務執行完後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,會讀取宏任務隊列中排在最前的任務,執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。棧空後,再次讀取微任務隊列裏的任務,依次類推。

接下來咱們看道例子來介紹上面流程:

Promise.resolve().then(()=>{
  console.log('Promise1')
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')
  })
},0)
複製代碼

最後輸出結果是 Promise1,setTimeout1,Promise2,setTimeout2

  • 一開始執行棧的同步任務(這屬於宏任務)執行完畢,會去查看是否有微任務隊列,上題中存在(有且只有一個),而後執行微任務隊列中的全部任務輸出 Promise1,同時會生成一個宏任務 setTimeout2

  • 而後去查看宏任務隊列,宏任務 setTimeout1 在 setTimeout2 以前,先執行宏任務 setTimeout1,輸出 setTimeout1

  • 在執行宏任務 setTimeout1 時會生成微任務 Promise2 ,放入微任務隊列中,接着先去清空微任務隊列中的全部任務,輸出 Promise2

  • 清空完微任務隊列中的全部任務後,就又會去宏任務隊列取一個,這回執行的是 setTimeout2

Node中的Event Loop

Node簡介

Node中的Event Loop和瀏覽器中的是徹底不相同的東西。Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現:

2019-01-14-004.png

Node.js的運行機制以下:

  • V8引擎解析JavaScript腳本。
  • 解析後的代碼,調用Node API。
  • libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎。
  • V8引擎在將結果返回給用戶。

六個階段

其中 libuv 引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。

2019-01-14-005.png

從上圖中,大體看出 node 中的事件循環的順序:

外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O 事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段(按照該順序反覆運行)...

  • timers 階段:這個階段執行 timer(setTimeout、setInterval)的回調
  • I/O callbacks 階段:處理一些上一輪循環中的少數未執行的 I/O 回調
  • idle, prepare 階段:僅 node 內部使用
  • poll 階段:獲取新的 I/O 事件, 適當的條件下 node 將阻塞在這裏
  • check 階段:執行 setImmediate() 的回調
  • close callbacks 階段:執行 socket 的 close 事件回調

注意:上面六個階段都不包括 process.nextTick()(下文會介紹)

接下去咱們詳細介紹timers、poll、check這 3 個階段,由於平常開發中的絕大部分異步任務都是在這 3 個階段處理的。

timer

timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。

一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行

poll

poll 是一個相當重要的階段,這一階段中,系統會作兩件事情

  • 回到timer執行階段回調
  • 執行I/O回調

而且在進入該階段時若是沒有設定了 timer 的話,會發生如下兩件事情

  • 若是 poll 隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或者達到系統限制
  • 若是 poll 隊列爲空時,會有兩件事發生
    • 若是有 setImmediate 回調須要執行,poll 階段會中止而且進入到 check 階段執行回調
    • 若是沒有 setImmediate 回調須要執行,會等待回調被加入到隊列中並當即執行回調,這裏一樣會有個超時時間設置防止一直等待下去

固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。

check 階段

setImmediate()的回調會被加入 check 隊列中,從 event loop 的階段圖能夠知道,check 階段的執行順序在 poll 階段以後。

咱們先來看個例子:

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
複製代碼
  • 一開始執行棧的同步任務(這屬於宏任務)執行完畢後(依次打印出 start end,並將 2 個 timer 依次放入 timer 隊列),會先去執行微任務(這點跟瀏覽器端的同樣),因此打印出 promise3

  • 而後進入 timers 階段,執行 timer1 的回調函數,打印 timer1,並將 promise.then 回調放入 microtask 隊列,一樣的步驟執行 timer2,打印 timer2;這點跟瀏覽器端相差比較大,timers 階段有幾個 setTimeout/setInterval 都會依次執行,並不像瀏覽器端,每執行一個宏任務後就去執行一個微任務(關於 Node 與瀏覽器的 Event Loop 差別,下文還會詳細介紹)。

注意點

setTimeout 和 setImmediate

兩者很是類似,區別主要在於調用時機不一樣。

  • setImmediate 設計在 poll 階段完成時執行,即 check 階段;
  • setTimeout 設計在 poll 階段爲空閒時,且設定時間到達後執行,但它在 timer 階段執行
setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
複製代碼
  • 對於以上代碼來講,setTimeout 可能執行在前,也可能執行在後。

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的

    進入事件循環也是須要成本的,若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調

  • 若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了

但當兩者在異步 i/o callback 內部調用時,老是先執行 setImmediate,再執行 setTimeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout
複製代碼

在上述代碼中,setImmediate 永遠先執行。由於兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執行,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了。

process.nextTick

這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
複製代碼

Node 與瀏覽器的 Event Loop 差別

瀏覽器環境下,microtask 的任務隊列是每一個 macrotask 執行完以後執行。而在 Node.js 中,microtask 會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 隊列的任務。

2019-01-14-006.png

接下咱們經過一個例子來講明二者區別:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
複製代碼

瀏覽器端運行結果:timer1=>promise1=>timer2=>promise2

瀏覽器端的處理過程以下:

2019-01-14-007.gif

Node 端運行結果:timer1=>timer2=>promise1=>promise2

  • 全局腳本(main())執行,將 2 個 timer 依次放入 timer 隊列,main()執行完畢,調用棧空閒,任務隊列開始執行;

  • 首先進入 timers 階段,執行 timer1 的回調函數,打印 timer1,並將 promise1.then 回調放入 microtask 隊列,一樣的步驟執行 timer2,打印 timer2;

  • 至此,timer 階段執行結束,event loop 進入下一個階段以前,執行 microtask 隊列的全部任務,依次打印 promise一、promise2

Node 端的處理過程以下:

2019-01-14-008.gif

6、總結

瀏覽器和 Node 環境下,microtask 任務隊列的執行時機不一樣

  • Node 端,microtask 在事件循環的各個階段之間執行
  • 瀏覽器端,microtask 在事件循環的 macrotask 執行完以後執行

練習

setTimeout(()=>{
   console.log(1) 
},0)
let a=new Promise((resolve)=>{
    console.log(2)
    resolve()
}).then(()=>{
   console.log(3) 
}).then(()=>{
   console.log(4) 
})
console.log(5) 
複製代碼

以此輸出 2,5,3,4,1

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
複製代碼

promise1,then11,promise2,then21,then12,then23

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    return new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
複製代碼

Promise的第二個then至關因而掛在新Promise的最後一個then的返回值上。

promise1,then11,promise2,then21,then23,then12

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
new Promise((resolve,reject)=>{
    console.log("promise3")
    resolve()
}).then(()=>{
    console.log("then31")
})
複製代碼

promise1,promise3,then11,promise2,then31,then21,then12,then23

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}

async function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 
複製代碼

script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout

async1 能夠當作以下

funcation async1(){
    console.log("async1 start");
    new Promise((resolve)=>{
     console.log( 'async2');
    }).then(()=>{
        console.log("async1 end");
    })
}
複製代碼
async function async1() {
    console.log(1)
    await async2()
    console.log(2)
    return await 3
}
async function async2() {
    console.log(4)
}

setTimeout(function() {
    console.log(5)
}, 0)

async1().then(v => console.log(v))
new Promise(function(resolve) {
    console.log(6)
    resolve();
    console.log(7)
}).then(function() {
    console.log(8)
})
console.log(9)
複製代碼

1,4,6,7,9,2,8,3,5

疑惑

咱們知道Promise自己是一個異步方法,必須得在執行棧執行完了再去取它的值,所以,全部的返回值都得包一層異步setTimeout。那麼問題來了,爲何Promise的resolve被setTimeout包裹後就成了微任務,要知道setTimeout但是宏任務。

解析

在現代瀏覽器裏面,產生微任務有兩種方式。

  • 第一種是使用MutationObserver監控某個DOM節點,而後在經過JavaScript來修改這個節點,或者爲這個節點添加,刪除部分子節點,當DOM節點發生變化時,就會產生DOM變化記錄的微任務。

  • 第二種方式是使用Promise,當調用Promise.resolve()或者Promise.reject()的時候,也會產生微任務。

ECMAScript規範明確指出Promise必須以Promise Job形式加入job queues(也就是microtask)。Job Queue是ES6中新剔除的概念,創建在事件循環隊列之上。

相關文章
相關標籤/搜索