真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

瀏覽器與Node的事件循環(huán)(EventLoop)之間的區(qū)別有哪些

這篇文章將為大家詳細(xì)講解有關(guān)瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些,小編覺得挺實(shí)用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

創(chuàng)新互聯(lián)公司基于分布式IDC數(shù)據(jù)中心構(gòu)建的平臺為眾多戶提供西云機(jī)房 四川大帶寬租用 成都機(jī)柜租用 成都服務(wù)器租用。

一、線程與進(jìn)程

1. 概念

我們經(jīng)常說 JS 是單線程執(zhí)行的,指的是一個進(jìn)程里只有一個主線程,那到底什么是線程?什么是進(jìn)程?

官方的說法是:進(jìn)程是 CPU 資源分配的最小單位;線程是 CPU 調(diào)度的最小單位。這兩句話并不好理解,我們先來看張圖:

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

  • 進(jìn)程好比圖中的工廠,有單獨(dú)的專屬自己的工廠資源。

  • 線程好比圖中的工人,多個工人在一個工廠中協(xié)作工作,工廠與工人是 1:n 的關(guān)系。也就是說一個進(jìn)程由一個或多個線程組成,線程是一個進(jìn)程中代碼的不同執(zhí)行路線;

  • 工廠的空間是工人們共享的,這象征一個進(jìn)程的內(nèi)存空間是共享的,每個線程都可用這些共享內(nèi)存。

  • 多個工廠之間獨(dú)立存在。

2. 多進(jìn)程與多線程
  • 多進(jìn)程:在同一個時間里,同一個計(jì)算機(jī)系統(tǒng)中如果允許兩個或兩個以上的進(jìn)程處于運(yùn)行狀態(tài)。多進(jìn)程帶來的好處是明顯的,比如你可以聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進(jìn)程之間絲毫不會相互干擾。

  • 多線程:程序中包含多個執(zhí)行流,即在一個程序中可以同時運(yùn)行多個不同的線程來執(zhí)行不同的任務(wù),也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)。

以 Chrome 瀏覽器中為例,當(dāng)你打開一個 Tab 頁時,其實(shí)就是創(chuàng)建了一個進(jìn)程,一個進(jìn)程中可以有多個線程(下文會詳細(xì)介紹),比如渲染線程、JS 引擎線程、HTTP 請求線程等等。當(dāng)你發(fā)起一個請求時,其實(shí)就是創(chuàng)建了一個線程,當(dāng)請求結(jié)束后,該線程可能就會被銷毀。

二、瀏覽器內(nèi)核

簡單來說瀏覽器內(nèi)核是通過取得頁面內(nèi)容、整理信息(應(yīng)用 CSS)、計(jì)算和組合最終輸出可視化的圖像結(jié)果,通常也被稱為渲染引擎。

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

  • GUI 渲染線程

  • JavaScript 引擎線程

  • 定時觸發(fā)器線程

  • 事件觸發(fā)線程

  • 異步 http 請求線程

1. GUI 渲染線程
  • 主要負(fù)責(zé)頁面的渲染,解析 HTML、CSS,構(gòu)建 DOM 樹,布局和繪制等。

  • 當(dāng)界面需要重繪或者由于某種操作引發(fā)回流時,將執(zhí)行該線程。

  • 該線程與 JS 引擎線程互斥,當(dāng)執(zhí)行 JS 引擎線程時,GUI 渲染會被掛起,當(dāng)任務(wù)隊(duì)列空閑時,JS 引擎才會去執(zhí)行 GUI 渲染。

2. JS 引擎線程
  • 該線程當(dāng)然是主要負(fù)責(zé)處理 JavaScript 腳本,執(zhí)行代碼。

  • 也是主要負(fù)責(zé)執(zhí)行準(zhǔn)備好待執(zhí)行的事件,即定時器計(jì)數(shù)結(jié)束,或者異步請求成功并正確返回時,將依次進(jìn)入任務(wù)隊(duì)列,等待 JS 引擎線程的執(zhí)行。

  • 當(dāng)然,該線程與 GUI 渲染線程互斥,當(dāng) JS 引擎線程執(zhí)行 JavaScript 腳本時間過長,將導(dǎo)致頁面渲染的阻塞。

3. 定時器觸發(fā)線程
  • 負(fù)責(zé)執(zhí)行異步定時器一類的函數(shù)的線程,如: setTimeout,setInterval。

  • 主線程依次執(zhí)行代碼時,遇到定時器,會將定時器交給該線程處理,當(dāng)計(jì)數(shù)完畢后,事件觸發(fā)線程會將計(jì)數(shù)完畢后的事件加入到任務(wù)隊(duì)列的尾部,等待 JS 引擎線程執(zhí)行。

4. 事件觸發(fā)線程
  • 主要負(fù)責(zé)將準(zhǔn)備好的事件交給 JS 引擎線程執(zhí)行。

比如 setTimeout 定時器計(jì)數(shù)結(jié)束, ajax 等異步請求成功并觸發(fā)回調(diào)函數(shù),或者用戶觸發(fā)點(diǎn)擊事件時,該線程會將整裝待發(fā)的事件依次加入到任務(wù)隊(duì)列的隊(duì)尾,等待 JS 引擎線程的執(zhí)行。

5. 異步 http 請求線程
  • 負(fù)責(zé)執(zhí)行異步請求一類的函數(shù)的線程,如: Promise,axios,ajax 等。

  • 主線程依次執(zhí)行代碼時,遇到異步請求,會將函數(shù)交給該線程處理,當(dāng)監(jiān)聽到狀態(tài)碼變更,如果有回調(diào)函數(shù),事件觸發(fā)線程會將回調(diào)函數(shù)加入到任務(wù)隊(duì)列的尾部,等待 JS 引擎線程執(zhí)行。

三、瀏覽器中的 Event Loop

1. Micro-Task 與 Macro-Task

事件循環(huán)中的異步隊(duì)列有兩種:macro(宏任務(wù))隊(duì)列和 micro(微任務(wù))隊(duì)列。宏任務(wù)隊(duì)列可以有多個,微任務(wù)隊(duì)列只有一個

  • 常見的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整體代碼)、 I/O 操作、UI 渲染等。

  • 常見的 micro-task 比如: process.nextTick、new Promise().then(回調(diào))、MutationObserver(html5 新特性) 等。

2. Event Loop 過程解析

一個完整的 Event Loop 過程,可以概括為以下階段:

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

  • 一開始執(zhí)行棧空,我們可以把執(zhí)行棧認(rèn)為是一個存儲函數(shù)調(diào)用的棧結(jié)構(gòu),遵循先進(jìn)后出的原則。micro 隊(duì)列空,macro 隊(duì)列里有且只有一個 script 腳本(整體代碼)。

  • 全局上下文(script 標(biāo)簽)被推入執(zhí)行棧,同步代碼執(zhí)行。在執(zhí)行的過程中,會判斷是同步任務(wù)還是異步任務(wù),通過對一些接口的調(diào)用,可以產(chǎn)生新的 macro-task 與 micro-task,它們會分別被推入各自的任務(wù)隊(duì)列里。同步代碼執(zhí)行完了,script 腳本會被移出 macro 隊(duì)列,這個過程本質(zhì)上是隊(duì)列的 macro-task 的執(zhí)行和出隊(duì)的過程。

  • 上一步我們出隊(duì)的是一個 macro-task,這一步我們處理的是 micro-task。但需要注意的是:當(dāng) macro-task 出隊(duì)時,任務(wù)是一個一個執(zhí)行的;而 micro-task 出隊(duì)時,任務(wù)是一隊(duì)一隊(duì)執(zhí)行的。因此,我們處理 micro 隊(duì)列這一步,會逐個執(zhí)行隊(duì)列中的任務(wù)并把它出隊(duì),直到隊(duì)列被清空。

  • 執(zhí)行渲染操作,更新界面

  • 檢查是否存在 Web worker 任務(wù),如果有,則對其進(jìn)行處理

  • 上述過程循環(huán)往復(fù),直到兩個隊(duì)列都清空

我們總結(jié)一下,每一次循環(huán)都是一個這樣的過程:

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

當(dāng)某個宏任務(wù)執(zhí)行完后,會查看是否有微任務(wù)隊(duì)列。如果有,先執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù),如果沒有,會讀取宏任務(wù)隊(duì)列中排在最前的任務(wù),執(zhí)行宏任務(wù)的過程中,遇到微任務(wù),依次加入微任務(wù)隊(duì)列。??蘸?,再次讀取微任務(wù)隊(duì)列里的任務(wù),依次類推。

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

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

  • 一開始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢,會去查看是否有微任務(wù)隊(duì)列,上題中存在(有且只有一個),然后執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)輸出 Promise1,同時會生成一個宏任務(wù) setTimeout2

  • 然后去查看宏任務(wù)隊(duì)列,宏任務(wù) setTimeout1 在 setTimeout2 之前,先執(zhí)行宏任務(wù) setTimeout1,輸出 setTimeout1

  • 在執(zhí)行宏任務(wù) setTimeout1 時會生成微任務(wù) Promise2 ,放入微任務(wù)隊(duì)列中,接著先去清空微任務(wù)隊(duì)列中的所有任務(wù),輸出 Promise2

  • 清空完微任務(wù)隊(duì)列中的所有任務(wù)后,就又會去宏任務(wù)隊(duì)列取一個,這回執(zhí)行的是 setTimeout2

四、Node 中的 Event Loop

1. Node 簡介

Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西。Node.js 采用 V8 作為 js 的解析引擎,而 I/O 處理方面使用了自己設(shè)計(jì)的 libuv,libuv 是一個基于事件驅(qū)動的跨平臺抽象層,封裝了不同操作系統(tǒng)一些底層特性,對外提供統(tǒng)一的 API,事件循環(huán)機(jī)制也是它里面的實(shí)現(xiàn)(下文會詳細(xì)介紹)。

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

Node.js 的運(yùn)行機(jī)制如下:

  • V8 引擎解析 JavaScript 腳本。

  • 解析后的代碼,調(diào)用 Node API。

  • libuv 庫負(fù)責(zé) Node API 的執(zhí)行。它將不同的任務(wù)分配給不同的線程,形成一個 Event Loop(事件循環(huán)),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給 V8 引擎。

  • V8 引擎再將結(jié)果返回給用戶。

2. 六個階段

其中 libuv 引擎中的事件循環(huán)分為 6 個階段,它們會按照順序反復(fù)運(yùn)行。每當(dāng)進(jìn)入某一個階段的時候,都會從對應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值,就會進(jìn)入下一階段。

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

從上圖中,大致看出 node 中的事件循環(huán)的順序:

外部輸入數(shù)據(jù)-->輪詢階段(poll)-->檢查階段(check)-->關(guān)閉事件回調(diào)階段(close callback)-->定時器檢測階段(timer)-->I/O 事件回調(diào)階段(I/O callbacks)-->閑置階段(idle, prepare)-->輪詢階段(按照該順序反復(fù)運(yùn)行)...

  • timers 階段:這個階段執(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)

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

接下去我們詳細(xì)介紹timers、poll、check這 3 個階段,因?yàn)槿粘i_發(fā)中的絕大部分異步任務(wù)都是在這 3 個階段處理的。

(1) timer

timers 階段會執(zhí)行 setTimeout 和 setInterval 回調(diào),并且是由 poll 階段控制的。
同樣,在 Node 中定時器指定的時間也不是準(zhǔn)確時間,只能是盡快執(zhí)行。

(2) poll

poll 是一個至關(guān)重要的階段,這一階段中,系統(tǒng)會做兩件事情

  • 回到 timer 階段執(zhí)行回調(diào)

  • 執(zhí)行 I/O 回調(diào)

并且在進(jìn)入該階段時如果沒有設(shè)定了 timer 的話,會發(fā)生以下兩件事情

  • 如果 poll 隊(duì)列不為空,會遍歷回調(diào)隊(duì)列并同步執(zhí)行,直到隊(duì)列為空或者達(dá)到系統(tǒng)限制

  • 如果 poll 隊(duì)列為空時,會有兩件事發(fā)生

    • 如果有 setImmediate 回調(diào)需要執(zhí)行,poll 階段會停止并且進(jìn)入到 check 階段執(zhí)行回調(diào)

    • 如果沒有 setImmediate 回調(diào)需要執(zhí)行,會等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行回調(diào),這里同樣會有個超時時間設(shè)置防止一直等待下去

當(dāng)然設(shè)定了 timer 的話且 poll 隊(duì)列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執(zhí)行回調(diào)。

(3) check 階段

setImmediate()的回調(diào)會被加入 check 隊(duì)列中,從 event loop 的階段圖可以知道,check 階段的執(zhí)行順序在 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
  • 一開始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢后(依次打印出 start end,并將 2 個 timer 依次放入 timer 隊(duì)列),會先去執(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 階段有幾個 setTimeout/setInterval 都會依次執(zhí)行,并不像瀏覽器端,每執(zhí)行一個宏任務(wù)后就去執(zhí)行一個微任務(wù)(關(guān)于 Node 與瀏覽器的 Event Loop 差異,下文還會詳細(xì)介紹)。

3. 注意點(diǎn)

(1) setTimeout 和 setImmediate

二者非常相似,區(qū)別主要在于調(diào)用時機(jī)不同。

  • setImmediate 設(shè)計(jì)在 poll 階段完成時執(zhí)行,即 check 階段;

  • setTimeout 設(shè)計(jì)在 poll 階段為空閑時,且設(shè)定時間到達(dá)后執(zhí)行,但它在 timer 階段執(zhí)行

setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
  • 對于以上代碼來說,setTimeout 可能執(zhí)行在前,也可能執(zhí)行在后。

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的
    進(jìn)入事件循環(huán)也是需要成本的,如果在準(zhǔn)備時候花費(fèi)了大于 1ms 的時間,那么在 timer 階段就會直接執(zhí)行 setTimeout 回調(diào)

  • 如果準(zhǔn)備時間花費(fèi)小于 1ms,那么就是 setImmediate 回調(diào)先執(zhí)行了

但當(dāng)二者在異步 i/o callback 內(nèi)部調(diào)用時,總是先執(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)閮蓚€代碼寫在 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

這個函數(shù)其實(shí)是獨(dú)立于 Event Loop 之外的,它有一個自己的隊(duì)列,當(dāng)每個階段完成后,如果存在 nextTick 隊(duì)列,就會清空隊(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ì)列是每個 macrotask 執(zhí)行完之后執(zhí)行。而在 Node.js 中,microtask 會在事件循環(huán)的各個階段之間執(zhí)行,也就是一個階段執(zhí)行完畢,就會去執(zhí)行 microtask 隊(duì)列的任務(wù)。

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

接下我們通過一個例子來說明兩者區(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

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

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

Node 端運(yùn)行結(jié)果:timer1=>timer2=>promise1=>promise2

  • 全局腳本(main())執(zhí)行,將 2 個 timer 依次放入 timer 隊(duì)列,main()執(zhí)行完畢,調(diào)用??臻e,任務(wù)隊(duì)列開始執(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)入下一個階段之前,執(zhí)行 microtask 隊(duì)列的所有任務(wù),依次打印 promise1、promise2

Node 端的處理過程如下:

瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些

六、總結(jié)

瀏覽器和 Node 環(huán)境下,microtask 任務(wù)隊(duì)列的執(zhí)行時機(jī)不同

  • Node 端,microtask 在事件循環(huán)的各個階段之間執(zhí)行

  • 瀏覽器端,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行

關(guān)于瀏覽器與Node的事件循環(huán)(Event Loop)之間的區(qū)別有哪些就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。


網(wǎng)頁標(biāo)題:瀏覽器與Node的事件循環(huán)(EventLoop)之間的區(qū)別有哪些
瀏覽路徑:http://weahome.cn/article/poiedp.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部