做者簡介:nekron 螞蟻金服·數據體驗技術團隊html
一直以來,我對Event Loop的認知界定都是可知可不知的分級,所以僅僅保留淺顯的概念,從未真正學習過,直到看了這篇文章——《這一次,完全弄懂 JavaScript 執行機制》。該文做者寫的很是友好,從最小的例子展開,讓我獲益匪淺,但最後的示例牽扯出了chrome
和Node
下的運行結果迥異,我很好奇,我以爲有必要對這一塊知識進行學習。node
因爲上述緣由,本文誕生,本來我計劃全文共分3部分來展開:規範、實現、應用。但遺憾的是因爲本身的認知尚淺,在如何根據Event Loop的特性來設想應用場景時,實在沒有什麼產出,致使有關應用的篇幅太小,故不在標題中做體現了。git
(本文全部代碼運行環境僅包含Node v8.9.4以及 Chrome v63)github
由於Javascript設計之初就是一門單線程語言,所以爲了實現主線程的不阻塞,Event Loop這樣的方案應運而生。web
先來看一段代碼,打印結果會是?面試
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
複製代碼
不熟悉Event Loop的我嘗試進行以下分析:chrome
一、5
帶着困惑,我試着運行了一下代碼,正確結果是:一、五、三、四、2
。api
那這究竟是爲何呢?瀏覽器
看來須要先從規範定義入手,因而查閱一下HTML規範,規範着實詳(luo)細(suo),我就不貼了,提煉下來關鍵步驟以下:bash
好傢伙,問題還沒搞明白,一會兒又多出來2個概念task和microtask,讓懵逼的我更加凌亂了。。。
不慌不慌,經過仔細閱讀文檔得知,這兩個概念屬於對異步任務的分類,不一樣的API註冊的異步任務會依次進入自身對應的隊列中,而後等待Event Loop將它們依次壓入執行棧中執行。
task主要包含:setTimeout
、setInterval
、setImmediate
、I/O
、UI交互事件
microtask主要包含:Promise
、process.nextTick
、MutaionObserver
整個最基本的Event Loop如圖所示:
這時候,回頭再看下以前的測試(1)
,發現概念很是清晰,一會兒就得出了正確答案,感受本身萌萌噠,不再怕Event Loop了~
接着,準備挑戰一下更高難度的問題(本題出自序中提到的那篇文章,我先去除了process.nextTick
):
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
複製代碼
分析以下:
一、7
8
二、4
5
九、11
12
在chrome
下運行一下,全對!
自信的我膨脹了,準備加上process.nextTick
後在node上繼續測試。我先測試第一個task,代碼以下:
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
複製代碼
有了以前的積累,我這回自信的寫下了答案:一、七、八、六、二、四、五、3
。
然而,帥不過3秒,正確答案是:一、七、六、八、二、四、三、5
。
我陷入了困惑,不過很快明白了,這說明**process.nextTick
註冊的函數優先級高於Promise
**,這樣就全說的通了~
接着,我再測試第二個task:
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
setTimeout(() => {
console.log(9)
process.nextTick(() => {
console.log(10)
})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
複製代碼
吃一塹長一智,此次我掌握了microtask的優先級,因此答案應該是:
一、七、六、八、二、四、三、5
九、十一、十、12
然而,啪啪打臉。。。
我第一次執行,輸出結果是:一、七、六、八、二、四、九、十一、三、十、五、12
(即兩次task的執行混合在一塊兒了)。我繼續執行,有時候又會輸出我預期的答案。
現實真的是如此莫名啊!啊!啊!
(啊,很差意思,血一時止不住)因此,這究竟是爲何???
俗話說得好:
規範是人定的,代碼是人寫的。 ——無名氏
規範沒法囊括全部場景,雖然chrome
和node
都基於v8引擎,但引擎只負責管理內存堆棧,API仍是由各runtime自行設計並實現的。
Timer是整個Event Loop中很是重要的一環,咱們先從timer切入,來切身體會下規範和實現的差別。
首先再來一個小測試,它的輸出會是什麼呢?
setTimeout(() => {
console.log(2)
}, 2)
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log(0)
}, 0)
複製代碼
沒有深刻接觸過timer的同窗若是直接從代碼中的延時設置來看,會回答:0、一、2
。
而另外一些有必定經驗的同窗可能會回答:二、一、0
。由於MDN的setTimeout文檔中提到HTML規範最低延時爲4ms:
(補充說明:最低延時的設置是爲了給CPU留下休息時間)
In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.
而真正痛過的同窗會告訴你,答案是:一、0、2
。而且,不管是chrome
仍是node
下的運行結果都是一致的。
(錯誤訂正:經屢次驗證,node下的輸出順序依然是沒法保證的,node的timer真是一門玄學~)
從測試(3)
結果能夠看出,0ms和1ms的延時效果是一致的,那背後的緣由是爲何呢?咱們先查查blink
的實現。
(Blink代碼託管的地方我都不知道如何進行搜索,還好文件名比較明顯,沒花過久,找到了答案)
(我直接貼出最底層代碼,上層代碼若有興趣請自行查閱)
// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp#93
double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
複製代碼
這裏interval就是傳入的數值,能夠看出傳入0和傳入1結果都是oneMillisecond,即1ms。
這樣解釋了爲什麼1ms和0ms行爲是一致的,那4ms究竟是怎麼回事?我再次確認了HTML規範,發現雖然有4ms的限制,可是是存在條件的,詳見規範第11點:
If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
而且有意思的是,MDN英文文檔的說明也已經貼合了這個規範。
我斗膽推測,一開始HTML5規範確實有定最低4ms的規範,不過在後續修訂中進行了修改,我認爲甚至不排除規範在向實現看齊,即逆向影響。
那node
中,爲何0ms和1ms的延時效果一致呢?
(仍是github託管代碼看起來方便,直接搜到目標代碼)
// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1; // schedule on next tick, follows browser behavior
複製代碼
代碼中的註釋直接說明了,設置最低1ms的行爲是爲了向瀏覽器行爲看齊。
上文的timer算一個小插曲,咱們如今迴歸本文核心——Event Loop。
讓咱們聚焦在node
的實現上,blink
的實現本文不作展開,主要是由於:
chrome
行爲目前看來和規範一致(略過全部研究過程。。。)
直接看結論,下圖是node
的Event Loop實現:
補充說明:
Node
的Event Loop分階段,階段有前後,依次是
process.nextTick
註冊的函數瞭解了實現,再回頭看測試(2)
:
// 代碼簡略表示
// 1
setTimeout(() => {
// ...
})
// 2
setTimeout(() => {
// ...
})
複製代碼
能夠看出因爲兩個setTimeout
延時相同,被合併入了同一個expired timers queue,而一塊兒執行了。因此,只要將第二個setTimeout
的延時改爲超過2ms(1ms無效,詳見上文),就能夠保證這兩個setTimeout
不會同時過時,也可以保證輸出結果的一致性。
那若是我把其中一個setTimeout
改成setImmediate
,是否也能夠作到保證輸出順序?
答案是不能。雖然能夠保證setTimeout
和setImmediate
的回調不會混在一塊兒執行,但沒法保證的是setTimeout
和setImmediate
的回調的執行順序。
在node
下,看一個最簡單的例子,下面代碼的輸出結果是沒法保證的:
setTimeout(() => {
console.log(0)
})
setImmediate(() => {
console.log(1)
})
// or
setImmediate(() => {
console.log(0)
})
setTimeout(() => {
console.log(1)
})
複製代碼
問題的關鍵在於setTimeout
什麼時候到期,只有到期的setTimeout
才能保證在setImmediate
以前執行。
不過若是是這樣的例子(2)
,雖然基本能保證輸出的一致性,不過強烈不推薦:
// 先使用setTimeout註冊
setTimeout(() => {
// ...
})
// 一系列micro tasks執行,保證setTimeout順利到期
new Promise(resolve => {
// ...
})
process.nextTick(() => {
// ...
})
// 再使用setImmediate註冊,「幾乎」確保後執行
setImmediate(() => {
// ...
})
複製代碼
或者換種思路來保證順序:
const fs = require('fs')
fs.readFile('/path/to/file', () => {
setTimeout(() => {
console.log('timeout')
})
setImmediate(() => {
console.log('immediate')
})
})
複製代碼
那,爲什麼這樣的代碼能保證setImmediate
的回調優先於setTimeout
的回調執行呢?
由於當兩個回調同時註冊成功後,當前node
的Event Loop正處於I/O queue階段,而下一個階段是immediates queue,因此可以保證即便setTimeout
已經到期,也會在setImmediate
的回調以後執行。
因爲也是剛剛學習Event Loop,不管是依託於規範仍是實現,我能想到的應用場景還比較少。那掌握Event Loop,咱們能用在哪些地方呢?
正常狀況下,咱們不會碰到很是複雜的隊列場景。不過萬一碰到了,好比執行順序沒法保證的狀況時,咱們能夠快速定位到問題。
那何時會有複雜的隊列場景呢?好比面試,保不許會有這種稀奇古怪的測試,這樣就能輕鬆應付了~
說回正經的,若是從規範來看,microtask優先於task執行。那若是有須要優先執行的邏輯,放入microtask隊列會比task更早的被執行,這個特性能夠被用於在框架中設計任務調度機制。
若是從node
的實現來看,若是時機合適,microtask的執行甚至能夠阻塞I/O,是一把雙刃劍。
綜上,高優先級的代碼能夠用Promise
/process.nextTick
註冊執行。
從node
的實現來看,setTimeout
這種timer類型的API,須要建立定時器對象和迭代等操做,任務的處理須要操做小根堆,時間複雜度爲O(log(n))。而相對的,process.nextTick
和setImmediate
時間複雜度爲O(1),效率更高。
若是對執行效率有要求,優先使用process.nextTick
和setImmediate
。
歡迎你們一同補充~
對團隊感興趣的同窗能夠關注專欄或者發送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~