Node.js event loop 和 JS 瀏覽器環境下的事件循環的區別:javascript
1.線程與進程:html
JS 是單線程執行的,指的是一個進程裏只有一個主線程,那到底什麼是線程?什麼是進程?html5
進程是 CPU 資源分配的最小單位;線程是 CPU 調度的最小單位。java
一個進程由一個或多個線程組成,線程是一個進程中代碼的不一樣執行路線。node
一個進程的內存空間是共享的,每一個線程均可用這些共享內存。ios
2.多進程和多線程ajax
多進程:在同一個時間裏,同一個計算機系統中若是容許兩個或兩個以上的進程處於運行狀態。多進程帶來的好處是明顯的,好比你能夠聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進程之間絲絕不會相互干擾。axios
多線程:程序中包含多個執行流,即在一個程序中能夠同時運行多個不一樣的線程來執行不一樣的任務,也就是說容許單個程序建立多個並行執行的線程來完成各自的任務。promise
以 Chrome 瀏覽器中爲例,當你打開一個 Tab 頁時,其實就是建立了一個進程,一個進程中能夠有多個線程(下文會詳細介紹),好比渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是建立了一個線程,當請求結束後,該線程可能就會被銷燬。瀏覽器
3.瀏覽器
瀏覽器內核是經過取得頁面內容、整理信息(應用 CSS)、計算和組合最終輸出可視化的圖像結果,一般也被稱爲渲染引擎。
瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由如下常駐線程組成:
1. GUI 渲染線程
主要負責頁面的渲染,解析 HTML、CSS,構建 DOM 樹,佈局和繪製等。
當界面須要重繪或者因爲某種操做引起迴流時,將執行該線程。
該線程與 JS 引擎線程互斥,當執行 JS 引擎線程時,GUI 渲染會被掛起,當任務隊列空閒時,JS 引擎纔會去執行 GUI 渲染。
2. JS 引擎線程
該線程固然是主要負責處理 JavaScript 腳本,執行代碼。
也是主要負責執行準備好待執行的事件,即定時器計數結束,或者異步請求成功並正確返回時,將依次進入任務隊列,等待 JS 引擎線程的執行。
固然,該線程與 GUI 渲染線程互斥,當 JS 引擎線程執行 JavaScript 腳本時間過長,將致使頁面渲染的阻塞。
3. 定時器觸發線程
負責執行異步定時器一類的函數的線程,如: setTimeout,setInterval。
主線程依次執行代碼時,遇到定時器,會將定時器交給該線程處理,當計數完畢後,事件觸發線程會將計數完畢後的事件加入到任務隊列的尾部,等待 JS 引擎線程執行。
4. 事件觸發線程
主要負責將準備好的事件交給 JS 引擎線程執行。
好比 setTimeout 定時器計數結束, ajax 等異步請求成功並觸發回調函數,或者用戶觸發點擊事件時,該線程會將整裝待發的事件依次加入到任務隊列的隊尾,等待 JS 引擎線程的執行。
5. 異步 http 請求線程
負責執行異步請求一類的函數的線程,如: Promise,axios,ajax 等。
主線程依次執行代碼時,遇到異步請求,會將函數交給該線程處理,當監聽到狀態碼變動,若是有回調函數,事件觸發線程會將回調函數加入到任務隊列的尾部,等待 JS 引擎線程執行。
window.onload = function(){ console.log(1) setTimeout(function(){ console.log(2) },0) for (var i = 0; i < 10; i++) { if(i == 999) console.log(10) } console.log(4) }
上面代碼輸出結果爲1,3,4,2
事件循環中的異步隊列有兩種:macro(宏任務)隊列和 micro(微任務)隊列。宏任務隊列能夠有多個,微任務隊列只有一個。
常見的 macro-task 好比:setTimeout、setInterval、 setImmediate、script(總體代碼)、 I/O 操做、UI 渲染等。
常見的 micro-task 好比: process.nextTick、new Promise().then(回調)、MutationObserver(html5 新特性) 等。
全局上下文(script 標籤)被推入執行棧,同步代碼執行。在執行的過程當中,會判斷是同步任務仍是異步任務,經過對一些接口的調用,能夠產生新的 macro-task 與 micro-task,它們會分別被推入各自的任務隊列裏。同步代碼執行完了,script 腳本會被移出 macro 隊列,這個過程本質上是隊列的 macro-task 的執行和出隊的過程。
上一步咱們出隊的是一個 macro-task,這一步咱們處理的是 micro-task。但須要注意的是:當 macro-task 出隊時,任務是一個一個執行的;而 micro-task 出隊時,任務是一隊一隊執行的。所以,咱們處理 micro 隊列這一步,會逐個執行隊列中的任務並把它出隊,直到隊列被清空。
當某個宏任務執行完後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,會讀取宏任務隊列中排在最前的任務,執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。棧空後,再次讀取微任務隊列裏的任務,依次類推。
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
1.一開始執行棧的同步任務(這屬於宏任務)執行完畢,會去查看是否有微任務隊列,上題中存在(有且只有一個),而後執行微任務隊列中的全部任務輸出 Promise1,同時會生成一個宏任務 setTimeout2
2.而後去查看宏任務隊列,宏任務 setTimeout1 在 setTimeout2 以前,先執行宏任務 setTimeout1,輸出 setTimeout1
3.在執行宏任務 setTimeout1 時會生成微任務 Promise2 ,放入微任務隊列中,接着先去清空微任務隊列中的全部任務,輸出 Promise2
4.清空完微任務隊列中的全部任務後,就又會去宏任務隊列取一個,這回執行的是 setTimeout2
Node 中的 Event Loop 和瀏覽器中的是徹底不相同的東西。Node.js 採用 V8 做爲 js 的解析引擎,而 I/O 處理方面使用了本身設計的 libuv,libuv 是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的 API,事件循環機制也是它裏面的實現(下文會詳細介紹)
Node.js 的運行機制以下:
1.V8 引擎解析 JavaScript 腳本。
2.解析後的代碼,調用 Node API。
3.libuv 庫負責 Node API 的執行。它將不一樣的任務分配給不一樣的線程,造成一個 Event Loop(事件循環),以異步的方式將任務的執行結果返回給 V8 引擎。
4.V8 引擎再將結果返回給用戶。
六個階段
其中 libuv 引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。
node 中的事件循環的順序:
外部輸入數據–>輪詢階段(poll)–>檢查階段(check)–>關閉事件回調階段(close callback)–>定時器檢測階段(timer)–>I/O 事件回調階段(I/O callbacks)–>閒置階段(idle, prepare)–>輪詢階段(按照該順序反覆運行)…
1.timers 階段:這個階段執行 timer(setTimeout、setInterval)的回調
2.I/O callbacks 階段:處理一些上一輪循環中的少數未執行的 I/O 回調
3.idle, prepare 階段:僅 node 內部使用
4.poll 階段:獲取新的 I/O 事件, 適當的條件下 node 將阻塞在這裏
5.check 階段:執行 setImmediate() 的回調
6.close callbacks 階段:執行 socket 的 close 事件回調
上面六個階段都不包括 process.nextTick()
(1) timer
timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。
一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。
(2) poll
poll 是一個相當重要的階段,這一階段中,系統會作兩件事情
回到 timer 階段執行回調
執行 I/O 回調
而且在進入該階段時若是沒有設定了 timer 的話,會發生如下兩件事情
若是 poll 隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或者達到系統限制
若是 poll 隊列爲空時,會有兩件事發生
若是有 setImmediate 回調須要執行,poll 階段會中止而且進入到 check 階段執行回調
若是沒有 setImmediate 回調須要執行,會等待回調被加入到隊列中並當即執行回調,這裏一樣會有個超時時間設置防止一直等待下去
固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。
(3) 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 差別,下文還會詳細介紹)。
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
瀏覽器環境下,microtask 的任務隊列是每一個 macrotask 執行完以後執行。而在 Node.js 中,microtask 會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 隊列的任務。
參考連接:
https://blog.csdn.net/Fundebug/article/details/86487117
https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/