本文咱們將會介紹 JS 實現異步的原理,而且瞭解了在瀏覽器和 Node 中 Event Loop 實際上是不相同的。javascript
咱們常常說 JS 是單線程執行的,指的是一個進程裏只有一個主線程,那到底什麼是線程?什麼是進程?html
官方的說法是:進程是 CPU 資源分配的最小單位;線程是 CPU 調度的最小單位。這兩句話並很差理解,咱們先來看張圖:前端
以 Chrome 瀏覽器中爲例,當你打開一個 Tab 頁時,其實就是建立了一個進程,一個進程中能夠有多個線程(下文會詳細介紹),好比渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是建立了一個線程,當請求結束後,該線程可能就會被銷燬。html5
簡單來講瀏覽器內核是經過取得頁面內容、整理信息(應用 CSS)、計算和組合最終輸出可視化的圖像結果,一般也被稱爲渲染引擎。java
瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由如下常駐線程組成:node
好比 setTimeout 定時器計數結束, ajax 等異步請求成功並觸發回調函數,或者用戶觸發點擊事件時,該線程會將整裝待發的事件依次加入到任務隊列的隊尾,等待 JS 引擎線程的執行。ios
事件循環中的異步隊列有兩種:macro(宏任務)隊列和 micro(微任務)隊列。宏任務隊列能夠有多個,微任務隊列只有一個。git
一個完整的 Event Loop 過程,能夠歸納爲如下階段:github
一開始執行棧空,咱們能夠把執行棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。micro 隊列空,macro 隊列裏有且只有一個 script 腳本(總體代碼)。web
全局上下文(script 標籤)被推入執行棧,同步代碼執行。在執行的過程當中,會判斷是同步任務仍是異步任務,經過對一些接口的調用,能夠產生新的 macro-task 與 micro-task,它們會分別被推入各自的任務隊列裏。同步代碼執行完了,script 腳本會被移出 macro 隊列,這個過程本質上是隊列的 macro-task 的執行和出隊的過程。
上一步咱們出隊的是一個 macro-task,這一步咱們處理的是 micro-task。但須要注意的是:當 macro-task 出隊時,任務是一個一個執行的;而 micro-task 出隊時,任務是一隊一隊執行的。所以,咱們處理 micro 隊列這一步,會逐個執行隊列中的任務並把它出隊,直到隊列被清空。
執行渲染操做,更新界面
檢查是否存在 Web worker 任務,若是有,則對其進行處理
上述過程循環往復,直到兩個隊列都清空
咱們總結一下,每一次循環都是一個這樣的過程:
當某個宏任務執行完後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,會讀取宏任務隊列中排在最前的任務,執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。棧空後,再次讀取微任務隊列裏的任務,依次類推。
接下來咱們看道例子來介紹上面流程:
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
Node 中的 Event Loop 和瀏覽器中的是徹底不相同的東西。Node.js 採用 V8 做爲 js 的解析引擎,而 I/O 處理方面使用了本身設計的 libuv,libuv 是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的 API,事件循環機制也是它裏面的實現(下文會詳細介紹)。
Node.js 的運行機制以下:
其中 libuv 引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。
從上圖中,大體看出 node 中的事件循環的順序:
外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O 事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段(按照該順序反覆運行)...
注意:上面六個階段都不包括 process.nextTick()(下文會介紹)
接下去咱們詳細介紹timers
、poll
、check
這 3 個階段,由於平常開發中的絕大部分異步任務都是在這 3 個階段處理的。
(1) timer
timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。 一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。
(2) poll
poll 是一個相當重要的階段,這一階段中,系統會作兩件事情
而且在進入該階段時若是沒有設定了 timer 的話,會發生如下兩件事情
固然設定了 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
(1) setTimeout 和 setImmediate
兩者很是類似,區別主要在於調用時機不一樣。
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); });
但當兩者在異步 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 階段去執行回調了。
(2) 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 隊列的任務。
接下咱們經過一個例子來講明二者區別:
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
瀏覽器端的處理過程以下:
Node 端運行結果:timer1=>timer2=>promise1=>promise2
Node 端的處理過程以下:
瀏覽器和 Node 環境下,microtask 任務隊列的執行時機不一樣
Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用!
轉載時請註明做者Fundebug以及本文地址: https://blog.fundebug.com/2019/01/15/diffrences-of-browser-and-node-in-event-loop/