圖解搞懂JavaScript引擎Event Loop

一,js單線程存在的問題

js是單線程的,處理任務是一件接着一件處理,因此若是一個任務須要處理好久的話,後面的任務就會被阻塞html

因此js經過Event Loop事件循環的方式解決了這個問題,在瞭解事件循環前,咱們須要瞭解一些關鍵詞web

二,什麼是stack,queue,heap,event loop

  • stack(棧):吃多了吐
  • queue(隊列):吃多了...釋放
  • heap(堆):存儲obj對象

執行棧

js引擎運行時,當代碼開始運行的時候,會將代碼,壓入執行棧進行執行chrome

例: api

當代碼被解析後,函數會依次被壓入到棧中promise

有入棧,就要有出棧,當函數c執行完,開始出棧瀏覽器

當執行棧遇到異步

前面執行棧,先入後出,但其實也是同步的,同步就意味着會阻塞,因此須要異步,那當執行棧中出現異步代碼會怎麼樣bash

此時在代碼中,添加點擊事件和setTimeout,如今觀察一下執行順序併發

觀察此時的執行棧效果,和上面的函數嵌套有顯著區別異步

1,console.log("sync")的語句,不會被壓入到執行棧底部,由於console已經執行結束了函數

2,click和settimeout都入棧了,但它們內部的console沒有入棧的,這說明他們沒有執行完

3,若是click沒有執行完,那爲何setTimeout會入棧,不該該被阻塞嗎?

答案是:當瀏覽器在執行棧執行的時候,發現有異步任務以後,會交給webapi去維護,而執行棧則繼續執行後面的任務

一樣,setTimeout一樣會被添加到webapi中

webapi是瀏覽器本身實現的功能,這裏專門維護事件。

上面setTimeout旁邊有個進度條,這個進度就是設置的等待時間

回調隊列callback queue

上面的例子,當setTimeout執行結束的時候,是否是就應該回到執行棧,進行執行輸出呢?

答案:並非!

此時,倒計時結束後的setTimeout的可執行函數,被放入了回調隊列

最後,setTimeout的可執行函數,被從回調隊列中取出,再次放入了執行棧

這樣的執行過程就叫 event loop事件循環

Event Loop的具體流程

執行棧任務清空後,纔會從回調隊列頭部取出一個任務

上面是一個最簡單的例子,輸出結果是1,3,2

這是爲何?

上圖展現了具體的執行順序:

1,console.log(1)被壓入執行棧

2,setTimeout在執行棧被識別爲異步任務,放入webapis中

3,console.log(3)被壓入執行棧,此時setTimeout的可執行代碼還在回調隊列裏等待

4,console.log(3)執行完成後,從回調隊列頭部取出console.log(2),放入執行棧

5,console.log(2)執行

回調隊列先進先出

須要格外注意,回調隊列是先進先出的,例:

當console.log(4)執行完成後,從回調隊列裏取出了console.log(2);

注意:只有console.log(2)執行完成,執行棧再次清空時,纔會從回調隊列取出console.log(3)

測試概念是否正確

上面的代碼最後輸出1,5,2,4,3,執行過程:

1,輸出1,將2push進回調隊列

2,將4push進回調隊列

3,輸出5

4,清空了執行棧,讀取輸出2,發現有3,將3push進回調隊列

5,清空了執行棧,讀取輸出4

6,清空了執行棧,讀取輸出3

至此,看起來好像沒問題了,可是!!!!!!,事情尚未結束

Macrotask(宏任務)、Microtask(微任務)

經過上面的例子,想必已經對event loop有了必定的瞭解,如今繼續看一個例子

console.log(1);
setTimeout(()=>{
	console.log(2)
})
var p = new Promise((resolve,reject)=>{
	console.log(3)
	resolve("成功")
})
p.then(()=>{
	console.log(4)
})
console.log(5)
複製代碼

按照event loop的概念,應該是1,3,5,2,4,由於setTimeout和then會被放到回調隊列裏,而後又是先進先出,因此應該是2先輸出,4後輸出

但事實輸出的順序是1,3,5,4,2!

這是由於promise的then方法,被認爲是在Microtask微任務隊列當中

什麼是Macrotask(宏任務)

Macrotask(宏任務)很好理解,就是我們前面介紹過的回調隊列callback queue

什麼是Microtask(微任務)

Microtask(微任務)一樣是一個任務隊列,這個隊列的執行順序是在清空執行棧以後

用圖展現就是

能夠看到Macrotask(宏任務)也就是回調隊列上面還有一個Microtask(微任務)

Microtask(微任務)雖然是隊列,但並非一個一個放入執行棧,而是當執行棧請空,會執行所有Microtask(微任務)隊列中的任務,最後纔是取回調隊列的第一個Macrotask(宏任務)

例:

上面的執行過程是:

1,將setTimeout給push進宏任務

2,將then(2)push進微任務

3,將then(4)push進微任務

4,任務隊列爲空,取出微任務第一個then(2)壓入執行棧

5,輸出2,將then(3)push進微任務

6,任務隊列爲空,取出微任務第一個then(4)壓入執行棧

7,輸出4

8,任務隊列爲空,取出微任務第一個then(3)壓入執行棧

9,輸出3

10,任務隊列爲空,微任務也爲空,取出宏任務中的setTimeout(1)

11,輸出1

爲何then是微任務

這和每一個瀏覽器有關,每一個瀏覽器實現的promise不一樣,有的then是宏任務,有的是微任務,chrome是微任務,廣泛都默認爲微任務

除了then之外,還有幾個事件也被記爲微任務:

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver
console.log("start");
setImmediate(()=>{
    console.log(1)
})
Promise.resolve().then(()=>{
    console.log(4);
})
Promise.resolve().then(()=>{
    console.log(5);
})
process.nextTick(function foo() {
    console.log(2);
});
process.nextTick(function foo() {
    console.log(3);
});
console.log("end")
複製代碼

上面代碼輸出start,end,2,3,4,5,1

process.nextTick的概念和then不太同樣,process.nextTick是加入到執行棧底部,因此和其餘的表現並不一致

最後的測試

console.log("1");
setTimeout(()=>{
    console.log(2)
    Promise.resolve().then(()=>{
        console.log(3);
        process.nextTick(function foo() {
            console.log(4);
        });
    })
})
Promise.resolve().then(()=>{
    console.log(5);    
    setTimeout(()=>{
        console.log(6)
    })
    Promise.resolve().then(()=>{
        console.log(7);
    })
})

process.nextTick(function foo() {
    console.log(8);
    process.nextTick(function foo() {
        console.log(9);
    });
});
console.log("10")
複製代碼

執行順序:

1,輸出1

2,將setTimeout(2)push進宏任務

3,將then(5)push進微任務

4,在執行棧底部添加nextTick(8)

5,輸出10

6,執行nextTick(8)

7,輸出8

8,在執行棧底部添加nextTick(9)

9,輸出9

10,執行微任務then(5)

11,輸出5

12,將setTimeout(6)push進宏任務

13,將then(7)push進微任務

14,執行微任務then(7)

15,輸出7

16,取出setTimeout(2)

17,輸出2

18,將then(3)push進微任務

19,執行微任務then(3)

20,輸出3

21,在執行棧底部添加nextTick(4)

22,輸出4

23,取出setTimeout(6)

24,輸出6

最後結果是:1,10,8,9,5,7,2,3,4,6

參考

相關文章
相關標籤/搜索