最近在看js事件循環,事件循環是js運行的核心,js 是單線程的, js 的異步事件就是依賴於事件循環機制,網上找了些資料,發現騰訊雲這篇 js事件循環 寫的很詳細,下文基於這一篇文章,外加上本身的一些總結。javascript
首先,咱們來解釋下事件循環是個什麼東西: 就咱們所知,瀏覽器的js是單線程的,也就是說,在同一時刻,最多也只有一個代碼段在執行,但是瀏覽器又能很好的處理異步請求,那麼究竟是爲何呢?html
關於執行中的線程:前端
咱們來看一張圖(這張圖來自於http://www.zcfy.cc/article/node-js-at-scale-understanding-the-node-js-event-loop-risingstack-1652.html)java
從上圖咱們能夠看出,js主線程它是有一個執行棧的,全部的js代碼都會在執行棧裏運行。咱們看看瀏覽器上的執行棧node
在執行代碼過程當中,若是遇到一些異步代碼(好比setTimeout,ajax,promise.then以及用戶點擊等操做),那麼瀏覽器就會將這些代碼放到另外一個線程(在這裏咱們叫作幕後線程)中去執行,在前端由瀏覽器底層執行,在 node 端由 libuv 執行,這個線程的執行不阻塞主線程的執行,主線程繼續執行棧中剩餘的代碼。ajax
當幕後線程(background thread)裏的代碼執行完成後(好比setTimeout時間到了,ajax請求獲得響應),該線程就會將它的回調函數放到任務隊列(又稱做事件隊列、消息隊列)中等待執行。而當主線程執行完棧中的全部代碼後,它就會檢查任務隊列是否有任務要執行,若是有任務要執行的話,那麼就將該任務放到執行棧中執行。若是當前任務隊列爲空的話,它就會一直循環等待任務到來。所以,這叫作事件循環。chrome
那麼,問題來了。若是任務隊列中,有不少個任務的話,那麼要先執行哪個任務呢? 其實(正如上圖所示),js是有兩個任務隊列的,一個叫作 Macrotask Queue(Task Queue) 大任務, 一個叫作 Microtask Queue 小任務promise
Macrotask 常見的任務:瀏覽器
Microtask 常見的任務:網絡
那麼,二者有什麼具體的區別呢?或者說,若是兩種任務同時出現的話,應該選擇哪個呢?
其實事件循環執行流程以下:
簡而言之,一次事件循環只執行處於 Macrotask 隊首的任務,執行完成後,當即執行 Microtask 隊列中的全部任務。
咱們先來看一段代碼
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
複製代碼
你以爲結果應該是什麼呢? 我在node環境和chrome控制檯輸出的結果以下:
1
9
7
8
2
3
10
11
12
13
複製代碼
在上面的例子中
從macrotask隊列裏取位於隊首的任務(settimeout1)並執行,輸出2 microtask隊列爲空,回到第一步,進入下一個事件循環,此時macrotask隊列爲: setinterval1,settimeout2
從macrotask隊列裏取位於隊首的任務(setinterval1)並執行,輸出3,而後又將新生成的setinterval1加入macrotask隊列 microtask隊列爲空,回到第一步,進入下一個事件循環,此時macrotask隊列爲: settimeout2,setinterval1
從macrotask隊列裏取位於隊首的任務(settimeout2)並執行,輸出10,而且執行new Promise內的函數(new Promise內的函數是同步操做,並非異步操做),輸出11,而且將它的兩個then函數加入microtask隊列 從microtask隊列中,取隊首的任務執行,直到爲空爲止。所以,兩個新增的microtask任務按順序執行,輸出12和13,而且將setinterval1清空。
此時,microtask隊列和macrotask隊列都爲空,瀏覽器會一直檢查隊列是否爲空,等待新的任務加入隊列。 在這裏,你們能夠會想,在第一次循環中,爲何不是macrotask先執行?由於按照流程的話,不該該是先檢查macrotask隊列是否爲空,再檢查microtask隊列嗎?
緣由:由於一開始js主線程中跑的任務就是macrotask任務
,而根據事件循環的流程,一次事件循環只會執行一個macrotask任務,所以,執行完主線程的代碼後,它就去從microtask隊列裏取隊首任務來執行。
注意: 因爲在執行microtask任務的時候,只有當microtask隊列爲空的時候,它纔會進入下一個事件循環,所以,若是它源源不斷地產生新的microtask任務,就會致使主線程一直在執行microtask任務,而沒有辦法執行macrotask任務,這樣咱們就沒法進行UI渲染/IO操做/ajax請求了,所以,咱們應該避免這種狀況發生。在nodejs裏的process.nexttick裏,就能夠設置最大的調用次數,以此來防止阻塞主線程。
以此,咱們來引入一個新的問題,定時器的問題。定時器是不是真實可靠的呢?好比我執行一個命令:setTimeout(task, 100),他是否就能準確的在100毫秒後執行呢?其實根據以上的討論,咱們就能夠得知,這是不可能的。
咱們看這個栗子
const s = new Date().getSeconds();
setTimeout(function() {
// 輸出 "2",表示回調函數並無在 500 毫秒以後當即執行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
複製代碼
若是不知道事件循環機制,那麼想固然就認爲 setTimeout 中的事件會在 500 毫秒後執行,但其實是在 2 秒後才執行,緣由你們應該都知道了,主線程一直有任務在執行,直到 2 秒後,主線程中的任務才執行完成,這纔去執行 macrotask 中的 setTimeout 回調任務。
由於你執行 setTimeout(task,100) 後,其實只是確保這個任務,會在100毫秒後進入macrotask隊列,但並不意味着他能馬上運行,可能當前主線程正在進行一個耗時的操做,也可能目前microtask隊列有不少個任務,因此用 setTimeout 做爲倒計時其實並不會保證準確。
關於 js 阻塞仍是非阻塞的問題,我以爲能夠這麼理解,不夠在這以前,咱們先理解下同步、異步、阻塞仍是非阻塞的解釋,在網上看到一段描述的很是好,引用下
同步阻塞:小明一直盯着下載進度條,到 100% 的時候就完成。
同步非阻塞:小明提交下載任務後就去幹別的,每過一段時間就去瞄一眼進度條,看到 100% 就完成。(輪詢)
異步阻塞:小明換了個有下載完成通知功能的軟件,下載完成就「叮」一聲。不太小明仍然一直等待「叮」的聲音(看起來很傻,不是嗎最蠢)
異步非阻塞:仍然是那個會「叮」一聲的下載軟件,小明提交下載任務後就去幹別的,聽到「叮」的一聲就知道完成了。(最機智)
咱們的解釋:
while (true) {
if (new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
console.log('end')
複製代碼
console.log('end')
的執行須要在 while 循環結束後才能執行,若是循環一直沒結束,那麼線程就被阻塞了。