之前有看過一些事件循環的博客,不過一陣子沒看就發現自己忘光了,所以決定來自己寫一個博客總結下!
就我們所知,瀏覽器的js是單執行緒的,也就是說,在同一時刻,最多也只有一個程式碼段在執行,可是瀏覽器又能很好的處理非同步請求,那麼到底是為什麼呢?我們先來看一張圖
從上圖我們可以看出,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
#第二次事件循環:
microtask佇列為空,回到第一步,進入下一個事件循環,此時macrotask佇列為: setinterval1,settimeout2
#第三次事件循環:
microtask佇列為空,回到第一步,進入下一個事件循環,此時macrotask佇列為: settimeout2,setinterval1
第四次事件循環:
從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毫秒後執行呢?其實根據以上的討論,我們就可以得知,這是不可能的。
原因我想大家應該也都知道了,因為你執行setTimeout(task,100)後,其實只是確保這個任務,會在100毫秒後進入macrotask隊列,但並不代表他能立刻運行,可能目前主執行緒正在進行一個耗時的操作,也可能目前microtask佇列有很多個任務,所以這也可能是大家一直詬病setTimeout的原因吧哈哈哈哈
以上,只是我個人對事件循環的一些看法, 以及借鑒了其他優秀文章
以上是js事件循環的實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!