一,js單執行緒存在的問題
js是單執行緒的,處理任務是一件接著一件處理,所以如果一個任務需要處理很久的話,後面的任務就會被阻塞
所以js通過Event Loop事件迴圈的方式解決了這個問題,在瞭解事件迴圈前,我們需要了解一些關鍵詞
二,什麼是stack,queue,heap,event loop
- stack(棧):吃多了吐
- queue(佇列):吃多了...釋放
- heap(堆):儲存obj物件
執行棧
js引擎執行時,當程式碼開始執行的時候,會將程式碼,壓入執行棧進行執行
例:
當程式碼被解析後,函式會依次被壓入到棧中
有入棧,就要有出棧,當函式c執行完,開始出棧
當執行棧遇到非同步
前面執行棧,先入後出,但其實也是同步的,同步就意味著會阻塞,所以需要非同步,那當執行棧中出現非同步程式碼會怎麼樣
此時在程式碼中,新增點選事件和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