眾所周知,JavaScript(以下簡稱 JS) 是單執行緒語言,在 html5 中增加了 web workers,web workers 是新開了執行緒執行的,那麼 JS 還是單執行緒的嗎?當然是,為什麼要設計成單執行緒?
網上有很多說法,大部分都說是多個執行緒同時對一個dom操作(同時修改dom內容,一個執行緒增加屬性,一個執行緒刪除屬性),會非常混亂,當然如果支援多執行緒就會相應的就要加入多執行緒的鎖機制,那麼 JS 就變得非常複雜了,想想 JS 最開始設計的初衷就是用於使用者互動,而且當時的原始需求是:功能不需要太強,語法較為簡單,容易學習和部署,Brendan Eich 只用了10天,就設計完成了這種語言的第一版,因此也不可能加入多執行緒這麼複雜的技術。
即使現在支援 web workers,由於沒有多執行緒的機制,web workers 和執行執行緒只能通過 postMessage 來通訊,而且由於沒有鎖,web workers 無法訪問 window 和 document 物件。
JS 的單執行緒是指一個瀏覽器程式中只有一個 JS 的執行執行緒,即同一時刻內只會有一段程式碼在執行。
Micro-Task 與 Macro-Task
單執行緒如何實現非同步?JS 設計了一個事件迴圈的方式。所有的程式碼執行均按照事件迴圈的方式進行。
事件迴圈中分兩種任務:一個是巨集任務(Macro-Task),另一個是微任務(Micro-Task)。常見的巨集任務和微任務如下。
巨集任務:script(整體程式碼)、setTimeout、setInterval、requestAnimationFrame、I/O、事件、MessageChannel、setImmediate (Node.js) 微任務:Promise.then、 MutaionObserver、process.nextTick (Node.js)
事件迴圈按下圖的方式進行。
注意: 巨集任務執行完後,需要清空當前微任務佇列後才回去執行下一個巨集任務,如果微任務裡面產生了新的微任務,仍然會在當前事件迴圈裡面被執行完,後面會舉例說明。
來個示例驗證下上面的流程。
<script>
console.log(1);
setTimeout(function timeout1() {
console.log(2);
}, 0);
Promise.resolve().then(function promise1() {
console.log(3);
setTimeout(function timeout2() {
console.log(4);
Promise.resolve().then(function promise2() {
console.log(5);
});
}, 0);
return Promise.resolve()
.then(function promise3() {
console.log(6);
return Promise.resolve().then(function promise4() {
console.log(7);
});
})
.then(function promise5() {
console.log(8);
});
})
console.log(9);
</script>
<script>
console.log(10);
setTimeout(function timeout3() {
console.log(11);
}, 0);
Promise.resolve().then(function promise6() {
console.log(12);
});
</script>
複製程式碼
按照上面流程梳理下執行流程:
- 將兩個巨集任務(兩個script程式碼)初始化進巨集任務佇列,巨集任務佇列為:
[script1, script2]
- script1 出隊壓入執行棧執行,巨集任務佇列為:
[script2]
- 同步程式碼執行輸出:1,
- 0ms 後把 timeout1 放入巨集任務佇列,巨集任務佇列為:
[script2, timeout1]
- promise1 入隊,微任務佇列為:
[promise1]
- 同步程式碼執行輸出:9
- script1 執行完畢,進入微任務執行階段,promise1 出隊壓入執行棧執行,微任務佇列為空
- 同步程式碼執行輸出:3
- 0ms 後把 timeout2 放入巨集任務佇列,巨集任務佇列為:
[script2, timeout1, timeout2]
- promise3 入隊,微任務佇列為:
[promise3]
- promise1 執行完畢,繼續判斷微任務佇列是否為空,promise3 出隊壓入執行棧執行,微任務佇列為空
- 同步程式碼執行輸出:6
- promise4 入隊,微任務佇列為:
[promise4]
- promise3 執行完畢,promise5 入隊,微任務佇列為:
[promise4,promise5]
- 判斷微任務佇列是否為空,promise4 出隊壓入執行棧執行,微任務佇列為:
[promise5]
- 同步程式碼執行輸出:7
- promise4 執行完畢,繼續判斷微任務佇列是否為空,promise5 出隊壓入執行棧執行,微任務佇列為空
- 同步程式碼執行輸出:8
- 微任務佇列清空,巨集任務 script2 出隊壓入執行棧執行,巨集任務佇列為空
- 同步程式碼執行輸出:10
- 0ms 後把 timeout3 放入巨集任務佇列,巨集任務佇列為:
[timeout1, timeout2, timeout3]
- promise6 入隊,微任務佇列為:
[promise6]
- script2 執行完畢,進入微任務執行階段,promise6 出隊壓入執行棧執行,微任務佇列為空
- 同步程式碼執行輸出:12
- 微任務佇列為空,巨集任務 timeout1 壓入執行棧執行,巨集任務佇列為
[timeout2, timeout3]
- 同步程式碼執行輸出:2
- timeout1執行完畢,微任務佇列為空,巨集任務 timeout2 壓入執行棧執行,巨集任務佇列為
[timeout3]
- 同步程式碼執行輸出:4,promise2 入隊,微任務佇列為:
[promise2]
- timeout2 執行完畢,判斷微任務佇列是否為空,promise2 出隊壓入執行棧執行,微任務佇列為空
- 同步程式碼執行輸出:5
- promise2執行完,微任務佇列為空,巨集任務 timeout2 壓入執行棧執行,巨集任務佇列為空
- 同步程式碼執行輸出:11
- timeout3執行完畢,微任務佇列為空,巨集任務佇列為空
setTimeout
setTimeout 的 delay 最小值在不同瀏覽器的有差異,在 Chrome 74 上測試的結果是 2ms,Firefox 67 上測試的記過是 1ms。
最小值是什麼意思?就是小於這個值後,瀏覽器按照0處理。比如在 Chrome 上,測試下面的程式碼:
setTimeout(function(){console.log(1)},1.99);
setTimeout(function(){console.log(2)},0);
複製程式碼
輸出的結果為 1、2,而
setTimeout(function(){console.log(1)},2);
setTimeout(function(){console.log(2)},0);
複製程式碼
輸出的結果為 2、1,說明 2ms 是有效的。
另外 setTimeout 是從呼叫開始計時,到了時間就放入巨集任務佇列,我們來看下面的例子。
var s = Date.now()
setTimeout(function timeout1() {
console.log(1)
}, 200)
while (Date.now() - s <= 200) {
}
setTimeout(function timeout2() {
console.log(2)
}, 0)
複製程式碼
- timeout1 200ms 後會放入到巨集任務佇列中
- while 執行了 200ms,此時 timeout1 已經先新增到巨集任務佇列中,因此最終列印結果為:1、2
- 如果將 while 的時間設定小於 200ms,考慮到程式碼執行需要花費時間,將 while 的條件改為
Date.now() - s <= 198
- 測試 while 執行只花費了 198ms,timeout2 會被先新增到巨集任務佇列中,因此最終列印結果會是:2、1
setInterval
和 setTimeout 相同,呼叫開始計時,按 delay 時間將回撥新增到巨集任務佇列中。那麼 setInterval 是按 delay 不斷的向巨集任務佇列新增任務,還是需要等待已新增的任務執行完後再新增,還是其他機制?
思考下面程式碼:
var start = Date.now()
var id = setInterval(function interval() {
var whileStart = Date.now()
console.log(whileStart - start) // 輸出 interval1 呼叫的時間和最開始呼叫計時的時間差,即過了多久才呼叫
while (Date.now() - whileStart < 250) { // 相當於 sleep 250ms
}
}, 100)
setTimeout(function timeout() {
clearInterval(id)
console.log(Date.now() - start)
}, 400)
複製程式碼
列印的時間間隔是?
100
351
605
855
複製程式碼
為了更好的理解,用圖示來解釋上面的流程。