瀏覽器和NodeJS中的 Event Loop 事件循環(huán)


title: 瀏覽器和NodeJS中的 Event Loop 事件循環(huán)
date: 2018-12-06 23:06:23
tags: [JavaScript, NodeJS]
categories: JavaScript


前言

搞懂 Event Loop 花了我挺長時(shí)間的泉哈,提示本文較長莫绣,閱讀需有耐心顷编。

由于JavaScript是單線程的,當(dāng)有兩個(gè)任務(wù)執(zhí)行時(shí)后一個(gè)必須等前一個(gè)執(zhí)行完成后才能執(zhí)行吴裤,所以JavaScript會將任務(wù)分為兩類,同步任務(wù)異步任務(wù)(異步任務(wù)其實(shí)還可以分為 宏任務(wù)微任務(wù),這個(gè)后面會提)陶衅。
異步任務(wù)肯定是在同步任務(wù)之后的乃戈,但是異步任務(wù)之間又是怎么樣的一個(gè)順序呢褂痰,比如多個(gè)setTimeout事件又是怎么樣一個(gè)執(zhí)行順序?這就涉及到事件循環(huán):Event Loop症虑。
另外缩歪,瀏覽器和NodeJS的事件循環(huán)是不一樣的。

基本概念

首先來講清楚前言中的兩個(gè)概念谍憔。

  1. 同步任務(wù):指的是當(dāng)前在執(zhí)行棧(主線程)中運(yùn)行的任務(wù)匪蝙。只有當(dāng)前一個(gè)任務(wù)執(zhí)行完主籍,下一個(gè)任務(wù)才會接著執(zhí)行,不管前一個(gè)任務(wù)執(zhí)行需要多久逛球。
  2. 異步任務(wù):暫時(shí)不進(jìn)入執(zhí)行棧千元,而是先放到任務(wù)隊(duì)列中。當(dāng)執(zhí)行棧的任務(wù)執(zhí)行完并清空后颤绕,才會取出任務(wù)隊(duì)列中的任務(wù)到執(zhí)行棧中去執(zhí)行幸海。

JavaScript是單線程的。假如一個(gè)操作需花費(fèi)很長時(shí)間奥务,那么此時(shí)瀏覽器就會一直等待這個(gè)操作完成物独,就會造成不好的體驗(yàn)。因此汗洒,JS里有同步任務(wù)與異步任務(wù)议纯,這樣就避免了頁面堵塞

那么 JS 引擎怎么知道異步任務(wù)有沒有結(jié)果溢谤,能不能進(jìn)入主線程呢瞻凤?答案就是引擎在不停地檢查,一遍又一遍世杀,只要同步任務(wù)執(zhí)行完了阀参,引擎就會去檢查那些掛起來的異步任務(wù),是不是可以進(jìn)入執(zhí)行棧了瞻坝。這種循環(huán)檢查的機(jī)制蛛壳,就叫做事件循環(huán)(Event Loop)

瀏覽器環(huán)境下的 Event Loop

我們來看一下這段代碼:

function fn1() {
  console.log('in fn1...')
  fn2()
}

function fn2 () {
  console.log('in fn2...')
}

setTimeout(()=>{
  console.log('setTimeout1 run...')
}, 0)

setTimeout(()=>{
  console.log('setTimeout2 run...')
}, 1000)

fn1()

輸出:

in fn1...
in fn2...
setTimeout1 run...
setTimeout2 run...

執(zhí)行完同步任務(wù)后所刀,JS引擎來看我們的任務(wù)隊(duì)列衙荐。如果定時(shí)器到了,就立刻把其回調(diào)函數(shù)放到執(zhí)行棧里去執(zhí)行浮创。
這結(jié)果在預(yù)料之中忧吟,那再來看一下另一段代碼:

function execTime(t) {
  let start = Date.now()
  while(Date.now() - start < t){}
}

function fn() {
  console.log('1')
  fn1()
  console.log('3')
}

function fn1 () {
  console.log('2')
}

setTimeout(()=>{
  execTime(3000)
  console.log('setTimeout1 run1...')
}, 0)

setTimeout(()=>{
  console.log('setTimeout2 run2...')
}, 1000)

setTimeout(()=>{
  console.log('setTimeout3 run3...')
}, 2000)

fn()

這次的輸出為:

1
2
3
setTimeout1 run1...
setTimeout2 run2...
setTimeout3 run3...

這段代碼需要你自己來運(yùn)行一下,你會發(fā)現(xiàn)斩披,后三條結(jié)果是3秒后一起出現(xiàn)的溜族,你給定時(shí)器設(shè)定的時(shí)間其實(shí)并不準(zhǔn)確。

原因:當(dāng)同步任務(wù)執(zhí)行完后垦沉,第一個(gè)setTimeout的定時(shí)器瞬間就到了煌抒,它的回調(diào)函數(shù)被放到執(zhí)行棧中執(zhí)行。你看了代碼會知道厕倍,這個(gè)任務(wù)需要至少3秒才能執(zhí)行完寡壮。而在這期間,后兩個(gè)定時(shí)器的時(shí)間也到了,那么它們的回調(diào)函數(shù)也會被放到執(zhí)行棧中等待執(zhí)行诬像。但是屋群,因?yàn)榈谝粋€(gè)任務(wù)沒有執(zhí)行完,所以后面的任務(wù)需要等待坏挠。因此3秒后,這三條結(jié)果會同時(shí)出現(xiàn)邪乍。

根據(jù)這個(gè)原理降狠,可以聯(lián)想到如果你滾動一個(gè)頁面時(shí)經(jīng)常卡頓庇楞,不流暢榜配,那么就是你在onscroll的回調(diào)函數(shù)中寫了太多代碼,這些代碼需要執(zhí)行很長的時(shí)間吕晌。你每一次滾動就是一次觸發(fā)蛋褥,把回調(diào)函數(shù)放到任務(wù)隊(duì)列中,然后一個(gè)個(gè)取出來放到執(zhí)行棧中去執(zhí)行睛驳,但是你放的太快了烙心,每個(gè)任務(wù)執(zhí)行的時(shí)間又太長了,導(dǎo)致后續(xù)的滾動你希望能立刻看到效果乏沸,但實(shí)際上還沒有輪到它執(zhí)行淫茵,所以會感覺卡頓

同理蹬跃,像click事件或者AJAX中的onreadystatechange等等匙瘪,它們的回調(diào)函數(shù)放到任務(wù)隊(duì)列,也是一樣的邏輯蝶缀。


MacroTask 和 MicroTask

異步任務(wù)隊(duì)列還可以分為 宏任務(wù)隊(duì)列 與 微任務(wù)隊(duì)列丹喻。

概念:

  • 宏任務(wù)(MacroTask):
    包括 setTimeout、 setInterval翁都、 setImmediate碍论、 I/O、 UI渲染
  • 微任務(wù)(MicroTask):
    包括 Promise荐吵、 process.nextTick骑冗、 Object.observe、 MutationObserver

謹(jǐn)記:

  1. 先執(zhí)行 宏任務(wù) 再執(zhí)行 微任務(wù)先煎。
  2. new Promise(fn).then(success) 的 fn 是立即執(zhí)行的贼涩,而 success 會被放入微任務(wù)。

機(jī)制:

  • 首先會執(zhí)行宏任務(wù)薯蝎,如果宏任務(wù)中存在宏任務(wù)遥倦,則會把該任務(wù)放到宏任務(wù)隊(duì)列中。如果該任務(wù)里存在微任務(wù),則把微任務(wù)放在微任務(wù)隊(duì)列袒哥。
  • 在這個(gè)宏任務(wù)執(zhí)行完后缩筛,首先去看微任務(wù)隊(duì)列中是否有任務(wù),然后把微任務(wù)推到執(zhí)行棧中執(zhí)行堡称。
  • 執(zhí)行完微任務(wù)隊(duì)列瞎抛,這一次循環(huán)就結(jié)束了,然后再進(jìn)行在宏任務(wù)隊(duì)列中進(jìn)行下一個(gè)宏任務(wù)却紧,微任務(wù)桐臊,直至回調(diào)隊(duì)列清空。

再來看一段代碼:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

let promise = new Promise((resolve, reject)=>{
    console.log(1)
    resolve()
})
promise.then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

輸出:

script start
1
promise1
promise2
setTimeout

我們來分析一下晓殊,
第一次循環(huán):

  1. 先執(zhí)行同步代碼(也是宏任務(wù))断凶,然后setTimeout被放入宏任務(wù)隊(duì)列,promise1巫俺、promise2被放入微任務(wù)隊(duì)列认烁。
  2. 同步代碼執(zhí)行完,取出微任務(wù)隊(duì)列的promise1介汹、promise2放入執(zhí)行棧并執(zhí)行(因?yàn)橄群暝傥ⅲ┤次耍链说谝淮窝h(huán)結(jié)束假残。

第二次循環(huán):

  1. 取出宏任務(wù)setTimeout推入執(zhí)行棧執(zhí)行迅皇,如果它里面有微任務(wù),就放到微任務(wù)隊(duì)列等待被執(zhí)行(該代碼中沒有)米丘。
  2. 宏任務(wù)setTimeout執(zhí)行完赶撰,JS引擎去看微任務(wù)隊(duì)列(空)舌镶,至此循環(huán)結(jié)束。

NodeJS 中的 Event Loop

NodeJS中的事件循環(huán)跟瀏覽器環(huán)境下的不一樣豪娜。

當(dāng)NodeJS啟動時(shí)會做 3 件事

  1. 初始化 Event Loop
  2. 開始執(zhí)行你寫的腳本
  3. 開始處理 Event Loop

NodeJS 的 Event Loop 有 6 個(gè)階段

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  1. timers 階段:這個(gè)階段執(zhí)行 setTimeout 和 setInterval 的回調(diào)函數(shù)餐胀。
  2. I/O callbacks 階段:不在 timers 階段、close callbacks 階段和 check 階段這三個(gè)階段執(zhí)行的回調(diào)瘤载,都由此階段負(fù)責(zé)否灾,這幾乎包含了所有回調(diào)函數(shù)。
  3. idle, prepare 階段(譯注:看起來是兩個(gè)階段鸣奔,不過這不重要):event loop 內(nèi)部使用的階段(譯注:我們不用關(guān)心這個(gè)階段)
  4. poll 階段:獲取新的 I/O 事件墨技。在某些場景下 Node.js 會阻塞在這個(gè)階段。
  5. check 階段:執(zhí)行 setImmediate() 的回調(diào)函數(shù)挎狸。
  6. close callbacks 階段:執(zhí)行關(guān)閉事件的回調(diào)函數(shù)扣汪,如 socket.on('close', fn) 里的 fn。

其中最重要的是這三個(gè)階段:timers锨匆、poll和check

timers階段
計(jì)時(shí)器實(shí)際上是在指定多久以后可以執(zhí)行某個(gè)回調(diào)函數(shù)崭别,而不是指定某個(gè)函數(shù)的確切執(zhí)行時(shí)間。當(dāng)指定的時(shí)間達(dá)到后,計(jì)時(shí)器的回調(diào)函數(shù)會盡早被執(zhí)行茅主。如果操作系統(tǒng)很忙舞痰,或者 Node.js 正在執(zhí)行一個(gè)耗時(shí)的函數(shù),那么計(jì)時(shí)器的回調(diào)函數(shù)就會被推遲執(zhí)行诀姚。

poll 階段(輪詢階段)
poll 階段有兩個(gè)功能:

  1. 如果發(fā)現(xiàn)計(jì)時(shí)器的時(shí)間到了响牛,就繞回到 timers 階段執(zhí)行計(jì)時(shí)器的回調(diào)。
  2. 然后再赫段,執(zhí)行 poll 隊(duì)列里的回調(diào)娃善。

當(dāng) event loop 進(jìn)入 poll 階段,如果發(fā)現(xiàn)沒有計(jì)時(shí)器瑞佩,就會:

  1. 如果 poll 隊(duì)列不是空的,event loop 就會依次執(zhí)行隊(duì)列里的回調(diào)函數(shù)坯台,直到隊(duì)列被清空或者到達(dá) poll 階段的時(shí)間上限炬丸。
  2. 如果 poll 隊(duì)列是空的,就會:
  3. 如果有 setImmediate() 任務(wù)蜒蕾,event loop 就結(jié)束 poll 階段去往 check 階段稠炬。
  4. 如果沒有 setImmediate() 任務(wù),event loop 就會等待新的回調(diào)函數(shù)進(jìn)入 poll 隊(duì)列咪啡,并立即執(zhí)行它首启。

一旦 poll 隊(duì)列為空,event loop 就會檢查計(jì)時(shí)器有沒有到期撤摸,如果有計(jì)時(shí)器到期了毅桃,event loop 就會回到 timers 階段執(zhí)行計(jì)時(shí)器的回調(diào)。

check 階段
這個(gè)階段允許開發(fā)者在 poll 階段結(jié)束后立即執(zhí)行一些函數(shù)准夷。如果 poll 階段空閑了钥飞,同時(shí)存在 setImmediate() 任務(wù),event loop 就會進(jìn)入 check 階段衫嵌,執(zhí)行setImmediate() 回調(diào)读宙。

舉例分析(重點(diǎn))

  1. 開始運(yùn)行Event Loop后,timers階段會去看腳本里是否設(shè)置了定時(shí)器setTimeout楔绞,比如一個(gè)4ms延遲與一個(gè)100ms延遲的定時(shí)器结闸,把它放到timers隊(duì)列中,直接進(jìn)入到poll階段酒朵。
  2. 進(jìn)入到poll階段桦锄,poll階段會去看定時(shí)器時(shí)間是否到了。
  3. 此時(shí)如果4ms到了耻讽,就進(jìn)入后面的階段然后回到timers階段執(zhí)行4ms定時(shí)器的回調(diào)函數(shù)察纯。接著又重復(fù)了一遍上述過程。
  4. 此時(shí)如果4ms沒到,poll階段就去處理它隊(duì)列里的任務(wù)了饼记。直到4ms到了香伴,就循環(huán)到timers階段執(zhí)行回調(diào)。

但是這里就有問題了具则,如果poll階段處理的這個(gè)任務(wù)花費(fèi)超過100ms了即纲,雖然定時(shí)器到了,但它的回調(diào)會等poll處理完任務(wù)后立即循環(huán)進(jìn)入timers階段再執(zhí)行博肋。

  1. 從poll階段進(jìn)入check階段時(shí)低斋,主要是看是否有setImmediate() 任務(wù),如果有則立即執(zhí)行匪凡,然后再進(jìn)入close callbacks 階段膊畴,進(jìn)行循環(huán),進(jìn)入timers階段病游。

setImmediate() vs setTimeout()

setImmediate 和 setTimeout 很相似唇跨,但是其回調(diào)函數(shù)的調(diào)用時(shí)機(jī)卻不一樣。
setImmediate() 的作用是在當(dāng)前 poll 階段結(jié)束后調(diào)用一個(gè)函數(shù)衬衬。 setTimeout() 的作用是在一段時(shí)間后調(diào)用一個(gè)函數(shù)买猖。一般來說 setImmediate 會先于 setTimeout 執(zhí)行,但是第一次啟動的時(shí)候不一樣滋尉,這兩者的回調(diào)的執(zhí)行順序取決于 setTimeout 和 setImmediate 被調(diào)用時(shí)的環(huán)境玉控。

如果 setTimeout 和 setImmediate 都是在主模塊(main module)中被調(diào)用的,那么回調(diào)的執(zhí)行順序取決于當(dāng)前進(jìn)程的性能狮惜,這個(gè)性能受其他應(yīng)用程序進(jìn)程的影響高诺。

舉例來說,如果在主模塊中運(yùn)行下面的腳本讽挟,那么兩個(gè)回調(diào)的執(zhí)行順序是無法判斷的:

setTimeout(()=>{
    console.log('setTimeout')
},0)

setImmediate(()=>{
    console.log('setImmediate')
})

結(jié)果:

setTimeout
setImmediate

setImmediate
setTimeout

為什么會發(fā)生這種情況呢懒叛?
因?yàn)槲覀儐覰odeJS時(shí), NodeJS會做三件事, 初始化event loop,運(yùn)行腳本耽梅,開始event loop薛窥。運(yùn)行腳本與開始event loop這兩件事不是同時(shí)執(zhí)行的,它兩中間間隔多少并不清楚眼姐,這跟環(huán)境性能有關(guān)诅迷。然后要注意的一點(diǎn),setTimeout的延遲時(shí)間最小為4ms众旗,所以這里的0相當(dāng)于4罢杉。

  1. 可能兩者間隔5ms,當(dāng)進(jìn)入timers階段的時(shí)候贡歧,NodeJS發(fā)現(xiàn)滩租,4ms已經(jīng)過了赋秀,立即執(zhí)行setTimeout定時(shí)器回調(diào),然后執(zhí)行setImmediate律想。
  2. 也可能兩者間隔3ms猎莲,當(dāng)進(jìn)入timers階段的時(shí)候,NodeJS發(fā)現(xiàn)技即,4ms還沒過著洼,就進(jìn)入下一階段,一直到checked而叼,執(zhí)行setImmediate身笤,然后等到4ms時(shí)再執(zhí)行setTimeout。

process.nextTick()

從技術(shù)上來講 process.nextTick() 并不是 event loop 的一部分葵陵。實(shí)際上液荸,event loop 再次進(jìn)入循環(huán)前,會去先執(zhí)行process.nextTick()脱篙。

setTimeout(()=>{
    console.log('setTimeout')
},0)

setImmediate(()=>{
    console.log('setImmediate')
})

proces.nextTick(()=>{
    console.log('nextTick')
})

上述代碼中nextTick先于其它兩個(gè)執(zhí)行莹弊,Vue中有Vue.nextTick()方法就是類似的思想。


本文參考

Event Loop事件循環(huán)涡尘,GET!
Event Loop响迂、計(jì)時(shí)器考抄、nextTick

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蔗彤,隨后出現(xiàn)的幾起案子川梅,更是在濱河造成了極大的恐慌,老刑警劉巖然遏,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贫途,死亡現(xiàn)場離奇詭異,居然都是意外死亡待侵,警方通過查閱死者的電腦和手機(jī)丢早,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秧倾,“玉大人怨酝,你說我怎么就攤上這事∧窍龋” “怎么了农猬?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長售淡。 經(jīng)常有香客問我斤葱,道長慷垮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任揍堕,我火速辦了婚禮料身,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鹤啡。我一直安慰自己惯驼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布递瑰。 她就那樣靜靜地躺著祟牲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抖部。 梳的紋絲不亂的頭發(fā)上说贝,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音慎颗,去河邊找鬼乡恕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俯萎,可吹牛的內(nèi)容都是我干的傲宜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼夫啊,長吁一口氣:“原來是場噩夢啊……” “哼函卒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起撇眯,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤报嵌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后熊榛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锚国,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年玄坦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了血筑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡煎楣,死狀恐怖云挟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情转质,我是刑警寧澤园欣,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站休蟹,受9級特大地震影響沸枯,放射性物質(zhì)發(fā)生泄漏日矫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一绑榴、第九天 我趴在偏房一處隱蔽的房頂上張望哪轿。 院中可真熱鬧,春花似錦翔怎、人聲如沸窃诉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽飘痛。三九已至,卻和暖如春容握,著一層夾襖步出監(jiān)牢的瞬間宣脉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工剔氏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留塑猖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓谈跛,卻偏偏與公主長得像羊苟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子感憾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容