js事件循環2
首先,我們來解釋下事件循環是個什麽東西:
就我們所知,瀏覽器的js是單線程的,也就是說,在同一時刻,最多也只有一個代碼段在執行,可是瀏覽器又能很好的處理異步請求,那麽到底是為什麽呢?我們先來看一張圖(這張圖來自於http://www.zcfy.cc/article/node-js-at-scale-understanding-the-node-js-event-loop-risingstack-1652.html)
從上圖我們可以看出,js主線程它是有一個執行棧的,所有的js代碼都會在執行棧裏運行。在執行代碼過程中,如果遇到一些異步代碼(比如setTimeout,ajax,promise.then以及用戶點擊等操作),那麽瀏覽器就會將這些代碼放到一個線程(在這裏我們叫做幕後線程)中去等待,不阻塞主線程的執行,主線程繼續執行棧中剩余的代碼,當幕後線程(background thread)裏的代碼準備好了(比如setTimeout時間到了,ajax請求得到響應),該線程就會將它的回調函數放到任務隊列
那麽,問題來了。如果任務隊列中,有很多個任務的話,那麽要先執行哪一個任務呢?
其實(正如上圖所示),js是有兩個任務隊列的,一個叫做Macrotask Queue(Task Queue),一個叫做Microtask Queue
- 前者主要是進行一些比較大型的工作,常見的有setTimeout,setInterval,用戶交互操作,UI渲染等
- 後者主要是進行一些比較小型的工作,常見的有Promise,process.nextTick(nodejs)
那麽,兩者有什麽具體的區別呢?或者說,如果兩種任務同時出現的話,應該選擇哪一個呢?
其實事件循環做的事情如下:
- 檢查Macrotask 隊列是否為空,若不為空,則進行下一步,若為空,則跳到3
- 從Macrotask隊列中取隊首(在隊列時間最長)的任務進去執行棧中執行(僅僅一個),執行完後進入下一步
- 檢查Microtask隊列是否為空,若不為空,則進入下一步,否則,跳到1(開始新的事件循環)
- 從Microtask隊列中取隊首(在隊列時間最長)的任務進去事件隊列執行,執行完後,跳到3
其中,在執行代碼過程中新增的microtask任務會在當前事件循環周期內執行,而新增的macrotask任務只能等到下一個事件循環才能執行了(一個事件循環只執行一個macrotask)
首先,我們先來看一段代碼
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
在上面的例子中
第一次事件循環:
- console.log(1)被執行,輸出1
- settimeout1執行,加入macrotask隊列
- setinterval1執行,加入macrotask隊列
- settimeout2執行,加入macrotask隊列
- promise2執行,它的兩個then函數加入microtask隊列
- console.log(9)執行,輸出9
- 根據事件循環的定義,接下來會執行新增的microtask任務,按照進入隊列的順序,執行console.log(7)和console.log(8),輸出7和8
microtask隊列為空,回到第一步,進入下一個事件循環,此時macrotask隊列為: settimeout1,setinterval1,settimeout2
第二次事件循環:
- 從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裏,就可以設置最大的調用次數,以此來防止阻塞主線程。
引自:
https://www.cnblogs.com/chenjg/p/7000044.html
js事件循環2