JavaScript執行機制、事件循環

Event Loop曾經的理解

首先,JS是單線程語言,也就意味着同一個時間只能作一件事,那麼html

  • 爲何JavaScript不是多線程呢?這樣還能提升效率啊
假定JS同時有兩個線程,一個線程在某個DOM節點上編輯了內容,而另外一個線程刪除了這個節點,這時瀏覽器就很懵逼了,到底以執行哪一個操做呢?

因此,設計者把JS設計成單線程應該就很好理解了,爲了不相似上述操做的複雜性,這一特徵未來也不會變。編程

可是單線程有一個問題:一旦這個線程被阻塞就沒法繼續工做了,這確定是不行的segmentfault

因爲異步編程能夠實現「非阻塞」的調用效果,引入異步編程天然就是瓜熟蒂落的事情了,那麼promise

  • JS單線程如何實現異步的呢?

今天的主咖登場——事件循環(Event Loop),JS異步是經過的事件循環實現的,理解了Event Loop機制,就理解
了JS的執行機制。瀏覽器

先來段代碼:多線程

console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

for(let i = 3; i < 10000; i++){
    console.log(i)
}
執行結果:1 3 4 5 6 7 ... 9997 9998 9999 2

setTimeout裏的函數並無當即執行,咱們都知道這部分叫異步處理模塊,延遲了一段時間,知足必定條件後才執行併發

仔細想一想,咱們在JS裏一般把任務分爲「同步任務」和「異步任務」,它們有如下的執行順序:異步

  1. 判斷任務是同步的仍是異步的,若是是同步任務就進入主線程執行棧中,若是是異步任務就進入Event Table並註冊函數,當知足觸發條件後,進入Event Queue
  2. 只有等到主線程的同步任務執行完後,纔會去Event Queue中查找是否有可執行的異步任務,若有,則進入主線程執行

以上兩步循環執行,就是所謂的Event Loop,因此上述代碼裏:異步編程

console.log(1) 是同步任務,進入主線程,當即執行
setTimeout 是異步任務,進入Event Table,0ms後(實際時間可能有出入,見註文)進入Event Queue,等待進入主線程
for 是同步任務,進入主線程,當即執行
全部主線程任務執行完後,setTimeout從Event Queue進入主線程執行
*注:HTML5規範規定最小延遲時間不能小於4ms,即x若是小於4,會被當作4來處理。 不過不一樣瀏覽器的實現不同,好比,Chrome能夠設置1ms,IE11/Edge是4ms

這就是我以前對Event Loop的理解,可是自從看了這篇文章深刻理解JS引擎的執行機制顛覆了我對Event Loop認識三觀,看下面的代碼函數

Event Loop如今的理解

console.log("start")
setTimeout(()=>{
    console.log("setTimeout")
}, 0)
new Promise((resolve)=>{
    console.log("promise")
    resolve()
}).then(()=>{
    console.log("then")
})
console.log("end")

嘗試按照咱們上面的JS執行機制去分析:

console.log("start")是同步任務,進入主線程,當即執行 setTimeout是異步任務,進入Event
Table,知足觸發條件後進入Event Queue
new Promise是同步任務,進入主線程,當即執行
.then是異步任務,進入Event Table,知足觸發條件後進入Event Queue,排在Event Queue隊尾 console.log("end")是同步任務,進入主線程,當即執行

因此執行結果是:start > promise > end > setTimeout > then

But可是,親自跑了代碼結果倒是:start > promise > end > then > setTimeout

對比結果發現,難道Event Queue裏面的順序不是隊列的先進先出的順序嗎?仍是這塊執行時有什麼改變,事實就是,前面按照同步和異步任務劃分的方式並不許確,那麼怎麼劃分纔是準確的呢,先看圖(轉自穀雨JavaScript 異步、棧、事件循環、任務隊列):

圖片描述

咣咣咣~敲黑板,知識點,知識點,知識點:

Js 中,有兩類任務隊列: 宏任務隊列(macro tasks)和 微任務隊列(micro tasks)

宏任務隊列能夠有多個,微任務隊列只有一個。那麼什麼任務,會分到哪一個隊列呢?

宏任務:script(全局任務), setTimeout, setInterval, setImmediate, I/O, UI rendering.
微任務:process.nextTick, Promise的then或catch, Object.observer, MutationObserver.

那麼結合上面的流程圖和最初理解的執行機制,總結了一下更爲準確的JS執行機制:

  1. 取且僅取一個宏任務來執行(第一個宏任務就是script任務)。執行過程當中判斷是同步仍是異步任務,若是是同步任務就進入主線程執行棧中,若是是異步任務就進入異步處理模塊,這些異步處理模塊的任務當知足觸發條件後,進入任務隊列,進入任務隊列後,按照宏任務和微任務進行劃分,劃分完畢後,執行下一步。
  2. 若是微任務隊列不爲空,則依次取出微任務來執行,直到微任務隊列爲空(即當前loop全部微任務執行完),執行下一步。
  3. 進入下一輪loop或更新UI渲染。

Event Loop就是循環執行上面三步,接下來使用上面的結論分析個例子幫助理解

  • 微任務裏嵌套宏任務
console.log('第一輪');

setTimeout(() => {                    //爲了便於敘述時區分,標記爲 setTimeout1
    console.log('第二輪');
    Promise.resolve().then(() => {    //爲了便於敘述時區分,標記爲 then1
        console.log('A');
    })
}, 0);

setTimeout(() => {                    //爲了便於敘述時區分,標記爲 setTimeout2
    console.log('第三輪');
    console.log('B');
}, 0);

new Promise((resolve)=>{              //爲了便於敘述時區分,標記爲 Promise1
    console.log("C")
    resolve()
}).then(() => {                       //爲了便於敘述時區分,標記爲 then2
    Promise.resolve().then(() => {    //爲了便於敘述時區分,標記爲 then3
        console.log("D")
        setTimeout(() => {            //爲了便於敘述時區分,標記爲 setTimeout3
            console.log('第四輪');
            console.log('E');
        }, 0);
    });
});

執行結果:第一輪 > C > D > 第二輪 > A > 第三輪 > B > 第四輪 > E

分析:

loop1:
第一步:首先執行全局宏任務,裏面同步任務有下面兩個,都當即進入主線程執行完後出棧

1.console.log('第一輪')
2.Promise1

輸出 「第一輪」 > 「C」

異步任務有三個,分別進入相應的任務隊列:

1.setTimeout1,該任務按照劃分標準是 宏任務

setTimeout(() => {
    console.log('第二輪');
    Promise.resolve().then(() => {
        console.log('A');
    })
}, 0);
2.setTimeout2,該任務按照劃分標準是 宏任務

setTimeout(() => {
    console.log('第三輪');
    console.log('B');
}, 0);
3.then2,該任務按照劃分標準是 微任務

.then(() => {
    Promise.resolve().then(() => {
        console.log("D")
        setTimeout(() => {
            console.log('第四輪');
            console.log('E');
        }, 0);
    });
});
因此此時宏任務隊列爲: setTimeout1,setTimeout2
微任務隊列爲: then2

第二步:loop1 微任務隊列不爲空,then2出隊列並執行,而後這個微任務裏的 then3繼續進入微任務隊列 ,setTimeout3進入宏任務隊列隊尾

那麼此時微任務隊列爲: then3
宏任務隊列爲: setTimeout1,setTimeout2,setTimeout3

可是此時第二步並無完,由於微任務隊列只要不爲空,就一直執行當前loop的微任務,因此從微任務隊列取出 then3 執行輸出 「D」

此時微任務隊列爲:
宏任務隊列爲: setTimeout1,setTimeout2,setTimeout3

到目前爲止,當前loop的微任務對列爲空,進入下一個loop,輸出狀況是「第一輪」 > 「C」 > 「D」

loop2:
第一步:在宏任務隊列隊首裏取出一個任務執行,即setTimeout1執行輸出「第二輪」,並then1進入微任務隊列

此時微任務隊列爲: then1
宏任務隊列爲: setTimeout2,setTimeout3

第二步:loop2 微任務隊列不爲空,則從微任務隊列取出then1執行,輸出「A」

此時微任務隊列爲:
宏任務隊列爲: setTimeout2,setTimeout3

到目前爲止,當前loop的微任務對列爲空,進入下一個loop,輸出狀況是「第一輪」 > 「C」 > 「D」 > 「第二輪」 > 「A」

loop3:
第一步:在宏任務隊列隊首裏取出一個任務執行,即setTimeout2執行輸出「第三輪」 > 「B」

此時微任務隊列爲:
宏任務隊列爲: setTimeout3

第二步:因爲loop3 微任務隊列爲空,則直接進入下一輪loop,輸出狀況是「第一輪」 > 「C」 > 「D」 > 「第二輪」 > 「A」 > 「第三輪」 > 「B」

loop4:
第一步:在宏任務隊列隊首裏取出一個任務執行,即setTimeout3執行輸出「第四輪」 > 「E」

此時微任務隊列爲:
宏任務隊列爲:

第二步:因爲loop4 微任務隊列爲空,宏任務隊列也爲空,則這次Event Loop結束,最終輸出狀況是「第一輪」 > 「C」 > 「D」 > 「第二輪」 > 「A」 > 「第三輪」 > 「B」 > 「第四輪」 > 「E」

上面的整個過程就是更爲準確的Event Loop,下面還有個不一樣的例子供讀者自行嘗試

  • 宏任務裏嵌套微任務
console.log('第一輪');

setTimeout(() => {
    console.log('第二輪');
    Promise.resolve().then(() => {
        console.log('A');
    })
}, 0);

setTimeout(() => {
    console.log('第三輪');
    console.log('B');
}, 0);

new Promise((resolve) => {
    console.log("C")
    resolve()
}).then(() => {                        //注意,這個函數改動啦
    setTimeout(() => {
        console.log('第四輪');
        console.log('E');
        Promise.resolve().then(() => {
            console.log("D")
        });
    }, 0);
});

執行結果:第一輪 > C > 第二輪 > A > 第三輪 > B > 第四輪 > E > D

Links:

深刻理解JS引擎的執行機制
JavaScript 異步、棧、事件循環、任務隊列
JavaScript 運行機制詳解:深刻理解Event Loop
JavaScript:併發模型與Event Loop
JavaScript 運行機制詳解:再談Event Loop[阮一峯]

相關文章
相關標籤/搜索