瀏覽器中的事件循環(huán)機制

原文地址在我的博客, 轉(zhuǎn)載注明來源

網(wǎng)上一搜事件循環(huán), 很多文章標(biāo)題的前面會加上 JavaScript, 但是我覺得事件循環(huán)機制跟 JavaScript 沒什么關(guān)系, JavaScript 只是一門解釋型語言, 方便開發(fā)和理解的, 由V8 JIT將 JavaScript 編譯成機器語言來調(diào)用底層, 至于瀏覽器怎么執(zhí)行 JavaScript 代碼, JavaScript 管不著也不關(guān)心. 因此, “JavaScript事件循環(huán)機制”這種說法是不合理的. 事件循環(huán)機制是由運行時環(huán)境實現(xiàn)的, 具體來說有瀏覽器蹲蒲、Node等. 這篇文章就先來說說瀏覽器中實現(xiàn)的事件循環(huán)機制.

正文

首先届搁,javascript 在瀏覽器端運行是單線程的,這是由瀏覽器決定的宴胧,這是為了避免多線程執(zhí)行不同任務(wù)會發(fā)生沖突的情況恕齐。也就是說我們寫的javascript 代碼只在一個線程上運行瞬逊,稱之為主線程(HTML5提供了web worker API可以讓瀏覽器開一個線程運行比較復(fù)雜耗時的 javascript任務(wù)确镊,但是這個線程仍受主線程的控制)。單線程的話拷肌,如果我們做一些“sleep”的操作比如說:

var now = + new Date()
while (+new Date() <= now + 1000){
//這是一個耗時的操所
}

那么在這將近一秒內(nèi)巨缘,線程就會被阻塞,無法繼續(xù)執(zhí)行下面的任務(wù)昔汉。

還有些操作比如說獲取遠(yuǎn)程數(shù)據(jù)拴清、I/O操作等口予,他們都很耗時,如果采用同步的方式煤辨,那么進(jìn)程在執(zhí)行這些操作時就會因為耗時而等待众辨,就像上面那樣舷礼,下面的任務(wù)也只能等待妻献,這樣效率并不高。

那瀏覽器是怎么做的呢?

我們找到WHATWG規(guī)范對Event loop的介紹:

WHATWG Event loop定義

為了協(xié)調(diào)事件谨履,用戶交互笋粟,腳本矗钟,渲染嫌变,網(wǎng)絡(luò)等躬它,用戶代理必須使用事件循環(huán)。

事件循環(huán)的主要機制就是任務(wù)隊列機制:

  • 一個事件循環(huán)有一個或者多個任務(wù)隊列(task queues)疮跑。任務(wù)隊列是task的有序列表凸舵,task是調(diào)度Events啊奄,Parsing,Callbacks菇夸,Using a resource庄新,Reacting to DOM manipulation這些任務(wù)的算法;
  • 每個任務(wù)都來自一個特定的任務(wù)源(task source)(比如鼠標(biāo)鍵盤事件)。來自同一個特定任務(wù)源且屬于特定事件循環(huán)的任務(wù)必須被加入到同一個任務(wù)隊列中械蹋,來自不同任務(wù)源的任務(wù)可以放在不同的任務(wù)隊列中;
  • 瀏覽器調(diào)用這些隊列中的任務(wù)時采取這樣的做法: 相同隊列中的任務(wù)按照先進(jìn)先出的順序, 不同的隊列按照提前設(shè)置的隊列優(yōu)先級來調(diào)用. 例如朝蜘,用戶代理可以有一個用于鼠標(biāo)和鍵盤事件的任務(wù)隊列(用戶交互任務(wù)源)谱醇,另一個用于其他任務(wù)步做。然后全度,用戶代理75%概率調(diào)用鍵盤和鼠標(biāo)事件任務(wù)隊列,25%調(diào)用其他隊列, 這樣的話就保持界面響應(yīng)而且不會餓死其他任務(wù)隊列. 但是相同隊列中的任務(wù)要按照先進(jìn)先出的順序勉盅。也就是說單獨的任務(wù)隊列中的任務(wù)總是按先進(jìn)先出的順序執(zhí)行草娜,但是不保證多個任務(wù)隊列中的任務(wù)優(yōu)先級痒筒,具體實現(xiàn)可能會交叉執(zhí)行

在調(diào)用任務(wù)的過程中, 會產(chǎn)生新的任務(wù), 瀏覽器就會不斷執(zhí)行任務(wù), 因此稱為事件循環(huán).

microtask queue 微任務(wù)隊列

還有一些特殊任務(wù), 它們不會被放在task queues中, 會放在一個叫做microtask(微任務(wù)) queue中, 繼續(xù)看標(biāo)準(zhǔn):

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.

任務(wù)隊列可以有多個, 但是微任務(wù)隊列只有一個.

那么哪些任務(wù)是放在task queue, 哪些放在microtask queue呢? 通常對瀏覽器和Node.js來說:

  • macrotask(宏任務(wù)): script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering等
  • microtask(微任務(wù)): process.nextTick, Promises(這里指瀏覽器實現(xiàn)的原生 Promise), Object.observe, MutationObserver

請尤其注意macrotask中執(zhí)行整體代碼也是一個宏任務(wù)

事件循環(huán)處理過程

總體來說, 瀏覽器端事件循環(huán)的一個回合(go-around或者叫cycle)就是:

  • 從macrotask隊列中(task queue)取一個宏任務(wù)執(zhí)行, 執(zhí)行完后, 取出所有的microtask執(zhí)行.
  • 重復(fù)回合

無論在執(zhí)行macrotask還是microtask, 都有可能產(chǎn)生新的macrotask或者microtask, 就這樣繼續(xù)執(zhí)行.

用任務(wù)隊列機制解釋異步操作順序

這里有一些常見異步操作:

const interval = setInterval(() => {
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve().then(() => {
    console.log('promise 3')
  }).then(() => {
    console.log('promise 4')
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
        clearInterval(interval)
      })
    }, 0)
  })
}, 0)

Promise.resolve().then(() => {
  console.log('promise 1')
}).then(() => {
  console.log('promise 2')
})

結(jié)果(Chrome 63.0.3239.84; Mac OS):

promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分情況下2次, 少數(shù)情況下一次
setTimeout 2
promise 5
promise 6

這個順序是如何得來的?

我們先講promise 4后面只出現(xiàn)一次setInterval的情況, 畫個圖簡單表示一下這個過程:

任務(wù)隊列機制

注意

本圖為了方便把各時間段(Cycle)隊列的任務(wù)都畫在隊列中去了, 實際上執(zhí)行一個task 和 microtask 后就會把這個任務(wù)從相應(yīng)隊列中刪除

首先, 主任務(wù)就是執(zhí)行腳本, 也就是執(zhí)行上述代碼, 這也是一個task. 在執(zhí)行代碼過程中, 遇到setTimeout、setInterval 就會將回調(diào)函數(shù)添加到task queue中, 遇到 promise 就會將then回調(diào)添加到 microtask 中去.

Task執(zhí)行完, 接著取所有 microtask 執(zhí)行, 所有microtask 執(zhí)行完了, microtask queue也就空了, 接著再取task執(zhí)行, 如果microtask queue為空, 沒有任務(wù), 則繼續(xù)取下一個task執(zhí)行, 就這樣循環(huán)執(zhí)行. 圖中箭頭就表示執(zhí)行的順序.

那么為什么promise 4后面大部分情況下出現(xiàn)2次setInterval, 少數(shù)情況出現(xiàn)1次呢?

我猜測這是因為setInterval是有最短間隔時間的(chrome下4ms左右), 這個時間不同機子螟左、不同瀏覽器都有可能不一樣. 代碼中的參數(shù)是0, 意味著盡可能短的時間內(nèi)就會產(chǎn)生一個task加入到 task queue中. 瀏覽器在執(zhí)行setInterval后到執(zhí)行下一個task前, 時間間隔就可能超過這個最短時間, 因此會產(chǎn)生一個setInterval task.

我是這樣論證的:

我把含有promise5、promise6回調(diào)函數(shù)的setTimeout的時間設(shè)置大一點, 讓它推遲插入task queue中:

...  
setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
        clearInterval(interval)
      })
}, 10)   //這里加上10ms 
...

結(jié)果是promise 4后面的setInterval出現(xiàn)了5次, 因此我覺得promise 4后面大部分情況下出現(xiàn)2次setInterval奄妨、少數(shù)情況出現(xiàn)一次的原因就是瀏覽器在執(zhí)行setInterval回調(diào)函數(shù)后苹祟、執(zhí)行setTimeout回調(diào)函數(shù)前, 時間間隔大部分情況超過了這個最短時間.

另外, 我試著再依次加上1ms, 直到14ms——也就是加上4ms時, promise 4后面的setInterval變成了6次, 可以認(rèn)為setInterval最短間隔時間在Chrome下約為4ms(不考慮機子性能树枫、設(shè)置).

Node中的奇怪結(jié)果

首先說明一下, 在Node中也體現(xiàn)了任務(wù)隊列的機制, 但是這不是Node實現(xiàn)的, 這是V8實現(xiàn)的, 由Node調(diào)用了V8任務(wù)隊列機制的API. 至于為什么是V8實現(xiàn)的, 我們翻翻ECMA 262 標(biāo)準(zhǔn)對 Job 和 Job queue 的介紹就可以得知

但是讓人摸不著頭腦的是, 這段代碼在node v8.5.0下有時會出現(xiàn)這樣的結(jié)果:

promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
setInterval   // 為什么會出現(xiàn)setInterval???
promise 5
promise 6

按理說應(yīng)該是setTimeout 2 => promise 5 => promise 6, 因為輸出setTimeout 2的回調(diào)函數(shù)是task, 執(zhí)行完這個task后應(yīng)該調(diào)用microtask 輸出promise 5 => promise 6啊? 很奇怪! Node對V8確實有些改動, 不知道是不是這方面原因...

還請大神解惑!

你竟然讀到這了

總結(jié)一下:

學(xué)習(xí)技術(shù)還是有捷徑的, 那就是讀標(biāo)準(zhǔn) ;)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奔誓,一起剝皮案震驚了整個濱河市搔涝,隨后出現(xiàn)的幾起案子庄呈,更是在濱河造成了極大的恐慌蜕煌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诬留,死亡現(xiàn)場離奇詭異斜纪,居然都是意外死亡,警方通過查閱死者的電腦和手機文兑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門盒刚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绿贞,你說我怎么就攤上這事因块。” “怎么了樟蠕?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵靠柑,是天一觀的道長寨辩。 經(jīng)常有香客問我吓懈,道長,這世上最難降的妖魔是什么靡狞? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任耻警,我火速辦了婚禮,結(jié)果婚禮上甸怕,老公的妹妹穿的比我還像新娘甘穿。我一直安慰自己,他們只是感情好梢杭,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布温兼。 她就那樣靜靜地躺著,像睡著了一般武契。 火紅的嫁衣襯著肌膚如雪募判。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天咒唆,我揣著相機與錄音届垫,去河邊找鬼。 笑死全释,一個胖子當(dāng)著我的面吹牛装处,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浸船,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼妄迁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了李命?” 一聲冷哼從身側(cè)響起登淘,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎项戴,沒想到半個月后形帮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡周叮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年辩撑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仿耽。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡合冀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出项贺,到底是詐尸還是另有隱情君躺,我是刑警寧澤峭判,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站棕叫,受9級特大地震影響林螃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜俺泣,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一疗认、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伏钠,春花似錦横漏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赴肚,卻和暖如春素跺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尊蚁。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工亡笑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人横朋。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓仑乌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親琴锭。 傳聞我的和親對象是個殘疾皇子晰甚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

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