這篇文章將為大家詳細(xì)講解有關(guān)瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來(lái)自于我們對(duì)這個(gè)行業(yè)的熱愛(ài)。我們立志把好的技術(shù)通過(guò)有效、簡(jiǎn)單的方式提供給客戶,將通過(guò)不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:主機(jī)域名、雅安服務(wù)器托管、營(yíng)銷軟件、網(wǎng)站建設(shè)、兗州網(wǎng)站維護(hù)、網(wǎng)站推廣。一、線程與進(jìn)程
1. 概念
我們經(jīng)常說(shuō) JS 是單線程執(zhí)行的,指的是一個(gè)進(jìn)程里只有一個(gè)主線程,那到底什么是線程?什么是進(jìn)程?
官方的說(shuō)法是:進(jìn)程是 CPU 資源分配的最小單位;線程是 CPU 調(diào)度的最小單位。這兩句話并不好理解,我們先來(lái)看張圖:
進(jìn)程好比圖中的工廠,有單獨(dú)的專屬自己的工廠資源。
線程好比圖中的工人,多個(gè)工人在一個(gè)工廠中協(xié)作工作,工廠與工人是 1:n 的關(guān)系。也就是說(shuō)一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成,線程是一個(gè)進(jìn)程中代碼的不同執(zhí)行路線;
工廠的空間是工人們共享的,這象征一個(gè)進(jìn)程的內(nèi)存空間是共享的,每個(gè)線程都可用這些共享內(nèi)存。
多個(gè)工廠之間獨(dú)立存在。
多進(jìn)程:在同一個(gè)時(shí)間里,同一個(gè)計(jì)算機(jī)系統(tǒng)中如果允許兩個(gè)或兩個(gè)以上的進(jìn)程處于運(yùn)行狀態(tài)。多進(jìn)程帶來(lái)的好處是明顯的,比如你可以聽(tīng)歌的同時(shí),打開(kāi)編輯器敲代碼,編輯器和聽(tīng)歌軟件的進(jìn)程之間絲毫不會(huì)相互干擾。
多線程:程序中包含多個(gè)執(zhí)行流,即在一個(gè)程序中可以同時(shí)運(yùn)行多個(gè)不同的線程來(lái)執(zhí)行不同的任務(wù),也就是說(shuō)允許單個(gè)程序創(chuàng)建多個(gè)并行執(zhí)行的線程來(lái)完成各自的任務(wù)。
以 Chrome 瀏覽器中為例,當(dāng)你打開(kāi)一個(gè) Tab 頁(yè)時(shí),其實(shí)就是創(chuàng)建了一個(gè)進(jìn)程,一個(gè)進(jìn)程中可以有多個(gè)線程(下文會(huì)詳細(xì)介紹),比如渲染線程、JS 引擎線程、HTTP 請(qǐng)求線程等等。當(dāng)你發(fā)起一個(gè)請(qǐng)求時(shí),其實(shí)就是創(chuàng)建了一個(gè)線程,當(dāng)請(qǐng)求結(jié)束后,該線程可能就會(huì)被銷毀。
二、瀏覽器內(nèi)核簡(jiǎn)單來(lái)說(shuō)瀏覽器內(nèi)核是通過(guò)取得頁(yè)面內(nèi)容、整理信息(應(yīng)用 CSS)、計(jì)算和組合最終輸出可視化的圖像結(jié)果,通常也被稱為渲染引擎。
瀏覽器內(nèi)核是多線程,在內(nèi)核控制下各線程相互配合以保持同步,一個(gè)瀏覽器通常由以下常駐線程組成:
GUI 渲染線程
JavaScript 引擎線程
定時(shí)觸發(fā)器線程
事件觸發(fā)線程
異步 http 請(qǐng)求線程
主要負(fù)責(zé)頁(yè)面的渲染,解析 HTML、CSS,構(gòu)建 DOM 樹(shù),布局和繪制等。
當(dāng)界面需要重繪或者由于某種操作引發(fā)回流時(shí),將執(zhí)行該線程。
該線程與 JS 引擎線程互斥,當(dāng)執(zhí)行 JS 引擎線程時(shí),GUI 渲染會(huì)被掛起,當(dāng)任務(wù)隊(duì)列空閑時(shí),JS 引擎才會(huì)去執(zhí)行 GUI 渲染。
該線程當(dāng)然是主要負(fù)責(zé)處理 JavaScript 腳本,執(zhí)行代碼。
也是主要負(fù)責(zé)執(zhí)行準(zhǔn)備好待執(zhí)行的事件,即定時(shí)器計(jì)數(shù)結(jié)束,或者異步請(qǐng)求成功并正確返回時(shí),將依次進(jìn)入任務(wù)隊(duì)列,等待 JS 引擎線程的執(zhí)行。
當(dāng)然,該線程與 GUI 渲染線程互斥,當(dāng) JS 引擎線程執(zhí)行 JavaScript 腳本時(shí)間過(guò)長(zhǎng),將導(dǎo)致頁(yè)面渲染的阻塞。
負(fù)責(zé)執(zhí)行異步定時(shí)器一類的函數(shù)的線程,如: setTimeout,setInterval。
主線程依次執(zhí)行代碼時(shí),遇到定時(shí)器,會(huì)將定時(shí)器交給該線程處理,當(dāng)計(jì)數(shù)完畢后,事件觸發(fā)線程會(huì)將計(jì)數(shù)完畢后的事件加入到任務(wù)隊(duì)列的尾部,等待 JS 引擎線程執(zhí)行。
主要負(fù)責(zé)將準(zhǔn)備好的事件交給 JS 引擎線程執(zhí)行。
比如 setTimeout 定時(shí)器計(jì)數(shù)結(jié)束, ajax 等異步請(qǐng)求成功并觸發(fā)回調(diào)函數(shù),或者用戶觸發(fā)點(diǎn)擊事件時(shí),該線程會(huì)將整裝待發(fā)的事件依次加入到任務(wù)隊(duì)列的隊(duì)尾,等待 JS 引擎線程的執(zhí)行。
5. 異步 http 請(qǐng)求線程負(fù)責(zé)執(zhí)行異步請(qǐng)求一類的函數(shù)的線程,如: Promise,axios,ajax 等。
主線程依次執(zhí)行代碼時(shí),遇到異步請(qǐng)求,會(huì)將函數(shù)交給該線程處理,當(dāng)監(jiān)聽(tīng)到狀態(tài)碼變更,如果有回調(diào)函數(shù),事件觸發(fā)線程會(huì)將回調(diào)函數(shù)加入到任務(wù)隊(duì)列的尾部,等待 JS 引擎線程執(zhí)行。
事件循環(huán)中的異步隊(duì)列有兩種:macro(宏任務(wù))隊(duì)列和 micro(微任務(wù))隊(duì)列。宏任務(wù)隊(duì)列可以有多個(gè),微任務(wù)隊(duì)列只有一個(gè)。
常見(jiàn)的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整體代碼)、 I/O 操作、UI 渲染等。
常見(jiàn)的 micro-task 比如: process.nextTick、new Promise().then(回調(diào))、MutationObserver(html5 新特性) 等。
一個(gè)完整的 Event Loop 過(guò)程,可以概括為以下階段:
一開(kāi)始執(zhí)行???我們可以把執(zhí)行棧認(rèn)為是一個(gè)存儲(chǔ)函數(shù)調(diào)用的棧結(jié)構(gòu),遵循先進(jìn)后出的原則。micro 隊(duì)列空,macro 隊(duì)列里有且只有一個(gè) script 腳本(整體代碼)。
全局上下文(script 標(biāo)簽)被推入執(zhí)行棧,同步代碼執(zhí)行。在執(zhí)行的過(guò)程中,會(huì)判斷是同步任務(wù)還是異步任務(wù),通過(guò)對(duì)一些接口的調(diào)用,可以產(chǎn)生新的 macro-task 與 micro-task,它們會(huì)分別被推入各自的任務(wù)隊(duì)列里。同步代碼執(zhí)行完了,script 腳本會(huì)被移出 macro 隊(duì)列,這個(gè)過(guò)程本質(zhì)上是隊(duì)列的 macro-task 的執(zhí)行和出隊(duì)的過(guò)程。
上一步我們出隊(duì)的是一個(gè) macro-task,這一步我們處理的是 micro-task。但需要注意的是:當(dāng) macro-task 出隊(duì)時(shí),任務(wù)是一個(gè)一個(gè)執(zhí)行的;而 micro-task 出隊(duì)時(shí),任務(wù)是一隊(duì)一隊(duì)執(zhí)行的。因此,我們處理 micro 隊(duì)列這一步,會(huì)逐個(gè)執(zhí)行隊(duì)列中的任務(wù)并把它出隊(duì),直到隊(duì)列被清空。
執(zhí)行渲染操作,更新界面
檢查是否存在 Web worker 任務(wù),如果有,則對(duì)其進(jìn)行處理
上述過(guò)程循環(huán)往復(fù),直到兩個(gè)隊(duì)列都清空
我們總結(jié)一下,每一次循環(huán)都是一個(gè)這樣的過(guò)程:
當(dāng)某個(gè)宏任務(wù)執(zhí)行完后,會(huì)查看是否有微任務(wù)隊(duì)列。如果有,先執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù),如果沒(méi)有,會(huì)讀取宏任務(wù)隊(duì)列中排在最前的任務(wù),執(zhí)行宏任務(wù)的過(guò)程中,遇到微任務(wù),依次加入微任務(wù)隊(duì)列。??蘸?,再次讀取微任務(wù)隊(duì)列里的任務(wù),依次類推。
接下來(lái)我們看道例子來(lái)介紹上面流程:
Promise.resolve().then(()=>{ console.log('Promise1') setTimeout(()=>{ console.log('setTimeout2') },0) }) setTimeout(()=>{ console.log('setTimeout1') Promise.resolve().then(()=>{ console.log('Promise2') }) },0)
最后輸出結(jié)果是 Promise1,setTimeout1,Promise2,setTimeout2
一開(kāi)始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢,會(huì)去查看是否有微任務(wù)隊(duì)列,上題中存在(有且只有一個(gè)),然后執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)輸出 Promise1,同時(shí)會(huì)生成一個(gè)宏任務(wù) setTimeout2
然后去查看宏任務(wù)隊(duì)列,宏任務(wù) setTimeout1 在 setTimeout2 之前,先執(zhí)行宏任務(wù) setTimeout1,輸出 setTimeout1
在執(zhí)行宏任務(wù) setTimeout1 時(shí)會(huì)生成微任務(wù) Promise2 ,放入微任務(wù)隊(duì)列中,接著先去清空微任務(wù)隊(duì)列中的所有任務(wù),輸出 Promise2
清空完微任務(wù)隊(duì)列中的所有任務(wù)后,就又會(huì)去宏任務(wù)隊(duì)列取一個(gè),這回執(zhí)行的是 setTimeout2
Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西。Node.js 采用 V8 作為 js 的解析引擎,而 I/O 處理方面使用了自己設(shè)計(jì)的 libuv,libuv 是一個(gè)基于事件驅(qū)動(dòng)的跨平臺(tái)抽象層,封裝了不同操作系統(tǒng)一些底層特性,對(duì)外提供統(tǒng)一的 API,事件循環(huán)機(jī)制也是它里面的實(shí)現(xiàn)(下文會(huì)詳細(xì)介紹)。
Node.js 的運(yùn)行機(jī)制如下:
V8 引擎解析 JavaScript 腳本。
解析后的代碼,調(diào)用 Node API。
libuv 庫(kù)負(fù)責(zé) Node API 的執(zhí)行。它將不同的任務(wù)分配給不同的線程,形成一個(gè) Event Loop(事件循環(huán)),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給 V8 引擎。
V8 引擎再將結(jié)果返回給用戶。
其中 libuv 引擎中的事件循環(huán)分為 6 個(gè)階段,它們會(huì)按照順序反復(fù)運(yùn)行。每當(dāng)進(jìn)入某一個(gè)階段的時(shí)候,都會(huì)從對(duì)應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值,就會(huì)進(jìn)入下一階段。
從上圖中,大致看出 node 中的事件循環(huán)的順序:
外部輸入數(shù)據(jù)-->輪詢階段(poll)-->檢查階段(check)-->關(guān)閉事件回調(diào)階段(close callback)-->定時(shí)器檢測(cè)階段(timer)-->I/O 事件回調(diào)階段(I/O callbacks)-->閑置階段(idle, prepare)-->輪詢階段(按照該順序反復(fù)運(yùn)行)...
timers 階段:這個(gè)階段執(zhí)行 timer(setTimeout、setInterval)的回調(diào)
I/O callbacks 階段:處理一些上一輪循環(huán)中的少數(shù)未執(zhí)行的 I/O 回調(diào)
idle, prepare 階段:僅 node 內(nèi)部使用
poll 階段:獲取新的 I/O 事件, 適當(dāng)?shù)臈l件下 node 將阻塞在這里
check 階段:執(zhí)行 setImmediate() 的回調(diào)
close callbacks 階段:執(zhí)行 socket 的 close 事件回調(diào)
注意:上面六個(gè)階段都不包括 process.nextTick()(下文會(huì)介紹)
接下去我們?cè)敿?xì)介紹timers
、poll
、check
這 3 個(gè)階段,因?yàn)槿粘i_(kāi)發(fā)中的絕大部分異步任務(wù)都是在這 3 個(gè)階段處理的。
(1) timer
timers 階段會(huì)執(zhí)行 setTimeout 和 setInterval 回調(diào),并且是由 poll 階段控制的。
同樣,在 Node 中定時(shí)器指定的時(shí)間也不是準(zhǔn)確時(shí)間,只能是盡快執(zhí)行。
(2) poll
poll 是一個(gè)至關(guān)重要的階段,這一階段中,系統(tǒng)會(huì)做兩件事情
回到 timer 階段執(zhí)行回調(diào)
執(zhí)行 I/O 回調(diào)
并且在進(jìn)入該階段時(shí)如果沒(méi)有設(shè)定了 timer 的話,會(huì)發(fā)生以下兩件事情
如果 poll 隊(duì)列不為空,會(huì)遍歷回調(diào)隊(duì)列并同步執(zhí)行,直到隊(duì)列為空或者達(dá)到系統(tǒng)限制
如果 poll 隊(duì)列為空時(shí),會(huì)有兩件事發(fā)生
如果有 setImmediate 回調(diào)需要執(zhí)行,poll 階段會(huì)停止并且進(jìn)入到 check 階段執(zhí)行回調(diào)
如果沒(méi)有 setImmediate 回調(diào)需要執(zhí)行,會(huì)等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行回調(diào),這里同樣會(huì)有個(gè)超時(shí)時(shí)間設(shè)置防止一直等待下去
當(dāng)然設(shè)定了 timer 的話且 poll 隊(duì)列為空,則會(huì)判斷是否有 timer 超時(shí),如果有的話會(huì)回到 timer 階段執(zhí)行回調(diào)。
(3) check 階段
setImmediate()的回調(diào)會(huì)被加入 check 隊(duì)列中,從 event loop 的階段圖可以知道,check 階段的執(zhí)行順序在 poll 階段之后。
我們先來(lái)看個(gè)例子:
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
一開(kāi)始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢后(依次打印出 start end,并將 2 個(gè) timer 依次放入 timer 隊(duì)列),會(huì)先去執(zhí)行微任務(wù)(這點(diǎn)跟瀏覽器端的一樣),所以打印出 promise3
然后進(jìn)入 timers 階段,執(zhí)行 timer1 的回調(diào)函數(shù),打印 timer1,并將 promise.then 回調(diào)放入 microtask 隊(duì)列,同樣的步驟執(zhí)行 timer2,打印 timer2;這點(diǎn)跟瀏覽器端相差比較大,timers 階段有幾個(gè) setTimeout/setInterval 都會(huì)依次執(zhí)行,并不像瀏覽器端,每執(zhí)行一個(gè)宏任務(wù)后就去執(zhí)行一個(gè)微任務(wù)(關(guān)于 Node 與瀏覽器的 Event Loop 差異,下文還會(huì)詳細(xì)介紹)。
(1) setTimeout 和 setImmediate
二者非常相似,區(qū)別主要在于調(diào)用時(shí)機(jī)不同。
setImmediate 設(shè)計(jì)在 poll 階段完成時(shí)執(zhí)行,即 check 階段;
setTimeout 設(shè)計(jì)在 poll 階段為空閑時(shí),且設(shè)定時(shí)間到達(dá)后執(zhí)行,但它在 timer 階段執(zhí)行
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); });
對(duì)于以上代碼來(lái)說(shuō),setTimeout 可能執(zhí)行在前,也可能執(zhí)行在后。
首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的
進(jìn)入事件循環(huán)也是需要成本的,如果在準(zhǔn)備時(shí)候花費(fèi)了大于 1ms 的時(shí)間,那么在 timer 階段就會(huì)直接執(zhí)行 setTimeout 回調(diào)
如果準(zhǔn)備時(shí)間花費(fèi)小于 1ms,那么就是 setImmediate 回調(diào)先執(zhí)行了
但當(dāng)二者在異步 i/o callback 內(nèi)部調(diào)用時(shí),總是先執(zhí)行 setImmediate,再執(zhí)行 setTimeout
const fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') }) }) // immediate // timeout
在上述代碼中,setImmediate 永遠(yuǎn)先執(zhí)行。因?yàn)閮蓚€(gè)代碼寫(xiě)在 IO 回調(diào)中,IO 回調(diào)是在 poll 階段執(zhí)行,當(dāng)回調(diào)執(zhí)行完畢后隊(duì)列為空,發(fā)現(xiàn)存在 setImmediate 回調(diào),所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了。
(2) process.nextTick
這個(gè)函數(shù)其實(shí)是獨(dú)立于 Event Loop 之外的,它有一個(gè)自己的隊(duì)列,當(dāng)每個(gè)階段完成后,如果存在 nextTick 隊(duì)列,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行。
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 差異
瀏覽器環(huán)境下,microtask 的任務(wù)隊(duì)列是每個(gè) macrotask 執(zhí)行完之后執(zhí)行。而在 Node.js 中,microtask 會(huì)在事件循環(huán)的各個(gè)階段之間執(zhí)行,也就是一個(gè)階段執(zhí)行完畢,就會(huì)去執(zhí)行 microtask 隊(duì)列的任務(wù)。
接下我們通過(guò)一個(gè)例子來(lái)說(shuō)明兩者區(qū)別:
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0)
瀏覽器端運(yùn)行結(jié)果:timer1=>promise1=>timer2=>promise2
瀏覽器端的處理過(guò)程如下:
Node 端運(yùn)行結(jié)果:timer1=>timer2=>promise1=>promise2
全局腳本(main())執(zhí)行,將 2 個(gè) timer 依次放入 timer 隊(duì)列,main()執(zhí)行完畢,調(diào)用??臻e,任務(wù)隊(duì)列開(kāi)始執(zhí)行;
首先進(jìn)入 timers 階段,執(zhí)行 timer1 的回調(diào)函數(shù),打印 timer1,并將 promise1.then 回調(diào)放入 microtask 隊(duì)列,同樣的步驟執(zhí)行 timer2,打印 timer2;
至此,timer 階段執(zhí)行結(jié)束,event loop 進(jìn)入下一個(gè)階段之前,執(zhí)行 microtask 隊(duì)列的所有任務(wù),依次打印 promise1、promise2
Node 端的處理過(guò)程如下:
六、總結(jié)瀏覽器和 Node 環(huán)境下,microtask 任務(wù)隊(duì)列的執(zhí)行時(shí)機(jī)不同
Node 端,microtask 在事件循環(huán)的各個(gè)階段之間執(zhí)行
瀏覽器端,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行
關(guān)于瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。