衆所周知,JavaScript(如下簡稱 JS) 是單線程語言,在 html5 中增長了 web workers,web workers 是新開了線程執行的,那麼 JS 仍是單線程的嗎?固然是,爲何要設計成單線程?javascript
網上有不少說法,大部分都說是多個線程同時對一個dom操做(同時修改dom內容,一個線程增長屬性,一個線程刪除屬性),會很是混亂,固然若是支持多線程就會相應的就要加入多線程的鎖機制,那麼 JS 就變得很是複雜了,想一想 JS 最開始設計的初衷就是用於用戶交互,並且當時的原始需求是:功能不須要太強,語法較爲簡單,容易學習和部署,Brendan Eich 只用了10天,就設計完成了這種語言的初版,所以也不可能加入多線程這麼複雜的技術。html
即便如今支持 web workers,因爲沒有多線程的機制,web workers 和執行線程只能經過 postMessage 來通訊,並且因爲沒有鎖,web workers 沒法訪問 window 和 document 對象。html5
JS 的單線程是指一個瀏覽器進程中只有一個 JS 的執行線程,即同一時刻內只會有一段代碼在執行。java
單線程如何實現異步?JS 設計了一個事件循環的方式。全部的代碼執行均按照事件循環的方式進行。web
事件循環中分兩種任務:一個是宏任務(Macro-Task),另外一個是微任務(Micro-Task)。常見的宏任務和微任務以下。promise
宏任務:script(總體代碼)、setTimeout、setInterval、requestAnimationFrame、I/O、事件、MessageChannel、setImmediate (Node.js) 微任務:Promise.then、 MutaionObserver、process.nextTick (Node.js)瀏覽器
事件循環按下圖的方式進行。bash
注意: 宏任務執行完後,須要清空當前微任務隊列後纔回去執行下一個宏任務,若是微任務裏面產生了新的微任務,仍然會在當前事件循環裏面被執行完,後面會舉例說明。多線程
來個示例驗證下上面的流程。dom
<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>
複製代碼
按照上面流程梳理下執行流程:
[script1, script2]
[script2]
[script2, timeout1]
[promise1]
[script2, timeout1, timeout2]
[promise3]
[promise4]
[promise4,promise5]
[promise5]
[timeout1, timeout2, timeout3]
[promise6]
[timeout2, timeout3]
[timeout3]
[promise2]
setTimeout 的 delay 最小值在不一樣瀏覽器的有差別,在 Chrome 74 上測試的結果是 2ms,Firefox 67 上測試的記過是 1ms。
最小值是什麼意思?就是小於這個值後,瀏覽器按照0處理。好比在 Chrome 上,測試下面的代碼:
setTimeout(function(){console.log(1)},1.99);
setTimeout(function(){console.log(2)},0);
複製代碼
輸出的結果爲 一、2,而
setTimeout(function(){console.log(1)},2);
setTimeout(function(){console.log(2)},0);
複製代碼
輸出的結果爲 二、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)
複製代碼
Date.now() - s <= 198
和 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
複製代碼
爲了更好的理解,用圖示來解釋上面的流程。