圖解搞懂JavaScript引擎Event Loop

百命發表於2018-01-22

圖解搞懂JavaScript引擎Event Loop

一,js單執行緒存在的問題

js是單執行緒的,處理任務是一件接著一件處理,所以如果一個任務需要處理很久的話,後面的任務就會被阻塞

所以js通過Event Loop事件迴圈的方式解決了這個問題,在瞭解事件迴圈前,我們需要了解一些關鍵詞

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

  • stack(棧):吃多了吐
  • queue(佇列):吃多了...釋放
  • heap(堆):儲存obj物件

執行棧

圖解搞懂JavaScript引擎Event Loop

js引擎執行時,當程式碼開始執行的時候,會將程式碼,壓入執行棧進行執行

例:

圖解搞懂JavaScript引擎Event Loop

當程式碼被解析後,函式會依次被壓入到棧中

圖解搞懂JavaScript引擎Event Loop

有入棧,就要有出棧,當函式c執行完,開始出棧

圖解搞懂JavaScript引擎Event Loop

當執行棧遇到非同步

前面執行棧,先入後出,但其實也是同步的,同步就意味著會阻塞,所以需要非同步,那當執行棧中出現非同步程式碼會怎麼樣

圖解搞懂JavaScript引擎Event Loop

此時在程式碼中,新增點選事件和setTimeout,現在觀察一下執行順序

圖解搞懂JavaScript引擎Event Loop

觀察此時的執行棧效果,和上面的函式巢狀有顯著區別

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

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

3,如果click沒有執行完,那為什麼setTimeout會入棧,不應該被阻塞嗎?

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

圖解搞懂JavaScript引擎Event Loop

同樣,setTimeout同樣會被新增到webapi中

圖解搞懂JavaScript引擎Event Loop

webapi是瀏覽器自己實現的功能,這裡專門維護事件。

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

回撥佇列callback queue

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

答案:並不是!

圖解搞懂JavaScript引擎Event Loop

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

最後,setTimeout的可執行函式,被從回撥佇列中取出,再次放入了執行棧

圖解搞懂JavaScript引擎Event Loop

這樣的執行過程就叫 event loop事件迴圈

Event Loop的具體流程

執行棧任務清空後,才會從回撥佇列頭部取出一個任務

圖解搞懂JavaScript引擎Event Loop

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

這是為什麼?

圖解搞懂JavaScript引擎Event Loop

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

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

2,setTimeout在執行棧被識別為非同步任務,放入webapis中

3,console.log(3)被壓入執行棧,此時setTimeout的可執行程式碼還在回撥佇列裡等待

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

5,console.log(2)執行

回撥佇列先進先出

需要格外注意,回撥佇列是先進先出的,例:

圖解搞懂JavaScript引擎Event Loop

圖解搞懂JavaScript引擎Event Loop

當console.log(4)執行完成後,從回撥佇列裡取出了console.log(2);

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

測試概念是否正確

圖解搞懂JavaScript引擎Event Loop

上面的程式碼最後輸出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!

圖解搞懂JavaScript引擎Event Loop

這是因為promise的then方法,被認為是在Microtask微任務佇列當中

什麼是Macrotask(巨集任務)

Macrotask(巨集任務)很好理解,就是我們們前面介紹過的回撥佇列callback queue

什麼是Microtask(微任務)

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

用圖展示就是

圖解搞懂JavaScript引擎Event Loop

可以看到Macrotask(巨集任務)也就是回撥佇列上面還有一個Microtask(微任務)

Microtask(微任務)雖然是佇列,但並不是一個一個放入執行棧,而是當執行棧請空,會執行全部Microtask(微任務)佇列中的任務,最後才是取回撥佇列的第一個Macrotask(巨集任務)

例:

圖解搞懂JavaScript引擎Event Loop

上面的執行過程是:

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

參考

相關文章