Node.js event loop 和 JS 瀏覽器環境下的事件循環的區別:
1.線程與進程:
JS 是單線程執行的,指的是一個進程里只有一個主線程,那到底什么是線程?什么是進程?
進程是 CPU 資源分配的最小單位;線程是 CPU 調度的最小單位。
一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線。
一個進程的內存空間是共享的,每個線程都可用這些共享內存。
2.多進程和多線程
多進程:在同一個時間里,同一個計算機系統中如果允許兩個或兩個以上的進程處於運行狀態。多進程帶來的好處是明顯的,比如你可以聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進程之間絲毫不會相互干擾。
多線程:程序中包含多個執行流,即在一個程序中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程序創建多個並行執行的線程來完成各自的任務。
以 Chrome 瀏覽器中為例,當你打開一個 Tab 頁時,其實就是創建了一個進程,一個進程中可以有多個線程(下文會詳細介紹),比如渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是創建了一個線程,當請求結束后,該線程可能就會被銷毀。
3.瀏覽器
瀏覽器內核是通過取得頁面內容、整理信息(應用 CSS)、計算和組合最終輸出可視化的圖像結果,通常也被稱為渲染引擎。
瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:
- GUI 渲染線程
- JavaScript 引擎線程
- 定時觸發器線程
- 事件觸發線程
- 異步 http 請求線程
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
瀏覽器的 Event-loop:
事件循環中的異步隊列有兩種: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 中的 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
Node 與瀏覽器的 Event Loop 差異
瀏覽器環境下,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/