JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。javascript
JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?html
因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。java
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。面試
單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。ajax
若是排隊是由於計算量大,CPU忙不過來,倒也算了,可是不少時候CPU是閒着的,由於IO設備(輸入輸出設備)很慢(好比Ajax操做從網絡讀取數據),不得不等着結果出來,再往下執行。數據庫
JavaScript語言的設計者意識到,這時主線程徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。promise
因而,全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。瀏覽器
具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)bash
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一但"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。
複製代碼
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重複。網絡
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。爲了更好地理解Event Loop,請看下圖
事件循環能夠簡單描述爲:函數入棧,當Stack中執行到異步任務的時候,就將他丟給WebAPIs,接着執行同步任務,直到Stack爲空; 在此期間WebAPIs完成這個事件,把回調函數放入CallbackQueue中等待; 當執行棧爲空時,Event Loop把Callback Queue中的一個任務放入Stack中,回到第1步。
接下來看一個異步函數執行的例子:
var start=new Date();
setTimeout(function cb(){
console.log("時間間隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
複製代碼
JS的異步有一個機制的,就是會分爲宏任務和微任務。宏任務和微任務會放到不一樣的event queue中,先將全部的宏任務放到一個event queue(macro-task),再將微任務放到一個event queue(micro-task)中。執行完宏任務以後,就會先從微任務中取這個回調函數執行。
講的詳細一點的話
最開始, 執行棧爲空, 微任務隊列爲空, 宏任務隊列有一個 script 標籤(內含總體代碼)
將第一個宏任務出隊, 這裏即爲上述的 script 標籤
總體代碼執行過程當中, 若是是同步代碼, 直接執行(函數執行的話會有入棧出棧操做), 若是是異步代碼, 會根據任務類型推入不一樣的任務隊列中(宏任務或微任務)
當執行棧執行完爲空時, 會去處理微任務隊列的任務, 將微任務隊列的任務一個個推入調用棧執行完
微任務執行完後,檢查是否須要從新渲染 UI。
...往返循環直到宏任務和微任務隊列爲空
總結一下上述循環機制的特色:
出隊一個宏任務 -> 調用棧爲空後, 執行一隊微任務 -> 更新界面渲染 -> 回到第一步
一個event loop有一個或者多個task隊列。task任務源很是寬泛,好比ajax的onload,click事件,基本上咱們常常綁定的各類事件都是task任務源,還有數據庫操做(IndexedDB ),須要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來講task任務源:
microtask 隊列和task 隊列有些類似,都是先進先出的隊列,由指定的任務源去提供任務,不一樣的是一個 event loop裏只有一個microtask 隊列。另外microtask執行時機和Macrotasks也有所差別
下圖是一個事件循環的流程
舉個簡單的例子,假設一個script標籤的代碼以下:Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
複製代碼
運行過程:
script裏的代碼被列爲一個task,放入task隊列。
循環1:
【task隊列:script ;microtask隊列:】
【task隊列:setTimeout1 setTimeout2;microtask隊列:promise1】
循環2:
【task隊列:setTimeout1 setTimeout2;microtask隊列:】
【task隊列:setTimeout2;microtask隊列:promise2】
(循環2中的 setTimeout2爲何不是跟在setTimeout1的後面輸出?
這裏我以爲應該是setTimeout1和setTimeout2不是在同一個task隊列中,
是兩個task隊列。在執行完setTimeout1的task隊列後,
event loop去檢查microtask隊列是否有事件,而且把事推入到主棧。)
複製代碼
【task隊列:setTimeout2;microtask隊列:】
【task隊列:;microtask隊列:】
注:有些文章說的一個事件循環的開始是先執行微任務再執行宏任務,有有些說的是先執行宏任務再執行微任務,我我的以爲這兩種只是見解的角度不一致
console.log('-------start--------');
setTimeout(() => {
console.log('setTimeout'); // 將回調代碼放入另外一個宏任務隊列
}, 0);
new Promise((resolve, reject) => {
for (let i = 0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(()=>{
console.log('Promise'); // 將回調代碼放入微任務隊列
})
console.log('-------end--------');
複製代碼
運行結果:
-------start--------
0
1
2
3
4
-------end--------
Promise
setTimeout
複製代碼
由EXP1,咱們能夠看出,當JS執行完主線程上的代碼,會去檢查在主線程上建立的微任務隊列,執行完微任務隊列以後纔會執行宏任務隊列上的代碼
運行順序:
主線程 => 主線程上建立的微任務 => 主線程上建立的宏任務
script裏的代碼被列爲一個task,放入task隊列。
循環1:
【task隊列:script ;microtask隊列:】
【task隊列:setTimeout ;microtask隊列:promise】
循環2:
【task隊列:setTimeout ;microtask隊列:】
【task隊列:;microtask隊列:】
setTimeout(_ => console.log('setTimeout4'))
new Promise(resolve => {
resolve()
console.log('Promise1')
}).then(_ => {
console.log('Promise3')
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
複製代碼
運行結果:
Promise1
2
Promise3
before timeout
also before timeout
setTimeout4
複製代碼
由EXP2,咱們能夠看出,在微任務隊列執行時建立的微任務,仍是會排在主線程上建立出的宏任務以前執行(由於微任務只有一條,自增鏈不斷的話 會一直往下執行微任務,不會被中斷)
運行順序:
主線程 => 主線程上建立的微任務1 => 微任務1上建立的微任務2 => 主線程上建立的宏任務
script裏的代碼被列爲一個task,放入task隊列。
循環1:
【task隊列:script ;microtask隊列:】
【task隊列:setTimeout4;microtask隊列:promise3】
【task隊列:setTimeout4;microtask隊列:before timeout】
【task隊列:setTimeout4;microtask隊列:also before timeout】
循環2:
【task隊列:setTimeout4 ;microtask隊列:before timeout】
【task隊列:;microtask隊列:】
// 宏任務隊列 1
setTimeout(() => {
// 宏任務隊列 1.1
console.log('timer_1');
setTimeout(() => {
// 宏任務隊列 3
console.log('timer_3')
}, 0)
new Promise(resolve => {
resolve()
console.log('new promise')
}).then(() => {
// 微任務隊列 1
console.log('promise then')
})
}, 0)
setTimeout(() => {
// 宏任務隊列 2.2
console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')
複製代碼
運行結果:
========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3
複製代碼
運行順序:
主線程(宏任務隊列 1)=> 宏任務隊列 1.1 => 微任務隊列 1 => 宏任務隊列 3=>宏任務隊列2.2
循環1:
【task隊列:script ;microtask隊列:】
【task隊列:timer_1,timer_2;microtask隊列:】
循環2
【task隊列::timer_1,timer_2;microtask隊列:】
【task隊列:timer_2,timer_3;microtask隊列:promise then】
循環3
【task隊列:timer_2,timer_3;microtask隊列:】
【task隊列:timer_3;microtask隊列:】
循環4
【task隊列:timer_3;microtask隊列:】
【task隊列:;microtask隊列:】
// 宏任務1
new Promise((resolve) => {
console.log('new Promise(macro task 1)');
resolve();
}).then(() => {
// 微任務1
console.log('micro task 1');
setTimeout(() => {
// 宏任務3
console.log('macro task 3');
}, 0)
})
setTimeout(() => {
// 宏任務2
console.log('macro task 2');
}, 0)
console.log('========== Sync queue(macro task 1) ==========');
複製代碼
運行結果:
========== Sync queue(macro task 1) ==========
new Promise(macro task 1)
micro task 1
macro task 2
macro task 3
複製代碼
異步宏任務隊列只有一個,當在微任務中建立一個宏任務以後,他會被追加到異步宏任務隊列上(跟主線程建立的異步宏任務隊列是同一個隊列)
運行順序:
主線程 => 主線程上建立的微任務 => 主線程上建立的宏任務 => 微任務中建立的宏任務
循環1:
【task隊列:script ;microtask隊列:】
【task隊列:macro task 2;microtask隊列:micro task 1】
循環2
循環2
【task隊列:macro task 3;microtask隊列:】
【task隊列:;microtask隊列:】
代碼
<div class="outer">
<div class="inner"></div>
</div>
複製代碼
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
複製代碼
點擊 inner,最終打印結果爲:
click
promise
click
promise
timeout
timeout
複製代碼
分析
爲何打印結果是這樣的呢?咱們來分析一下: (0)將 script 標籤內的代碼(宏任務)放入執行棧執行,執行完後,宏任務微任務隊列皆空。
(1)點擊 inner,onClick 函數入執行棧執行,打印 "click"。執行完後執行棧爲空,由於事件冒泡的緣故,事件觸發線程會將向上派發事件的任務放入宏任務隊列。
(2)遇到 setTimeout,在最小延遲時間後,將回調放入宏任務隊列。遇到 promise,將 then 的任務放進微任務隊列
(3)此時,執行棧再次爲空。開始清空微任務,打印 "promise"
(4)此時,執行棧再次爲空。從宏任務隊列拿出一個任務執行,即前面提到的派發事件的任務,也就是冒泡。
(5)事件冒泡到 outer,執行回調,重複上述 "click"、"promise" 的打印過程。
(6)從宏任務隊列取任務執行,這時咱們的宏任務隊列已經累計了兩個 setTimeout 的回調了,因此他們會在兩個 Event Loop 週期裏前後獲得執行。
能夠當作是:
function onClick() {
//模擬outer click事件
setTimeout(function(){onClick1()},0)
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
function onClick1() {
console.log('click1');
setTimeout(function() {
console.log('timeout1');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
});
}
//模擬inner click事件
onClick()
複製代碼
代碼
inner.click()
複製代碼
打印結果爲:
click
click
promise
promise
timeout
timeout
複製代碼
分析
依舊分析一下:
(0)將 script(宏任務)放入執行棧執行,執行到 inner.click() 的時候,執行 onClick 函數,打印 "click"
(1)當執行完 onClick 後,此時的 script(宏任務)還沒返回,執行棧不爲空,不會去清空微任務,而是會將事件往上冒泡派發
...(關鍵步驟分析完後,續步驟就不分析了)
能夠當作是:
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
onClick();
onClick();
複製代碼
總結
在通常狀況下,微任務的優先級是更高的,是會優先於事件冒泡的,但若是手動 .click() 會使得在 script代碼塊 還沒彈出執行棧的時候,觸發事件派發。
瀏覽器進行事件循環工做方式
選擇當前要執行的任務隊列,選擇任務隊列中最早進入的任務,若是任務隊列爲空即null,則執行跳轉到微任務(MicroTask)的執行步驟。
將事件循環中的任務設置爲已選擇任務。
執行任務。
將事件循環中當前運行任務設置爲null。
將已經運行完成的任務從任務隊列中刪除。
microtasks步驟:進入microtask檢查點。
更新界面渲染。
返回第一步
【執行進入microtask檢查點時,瀏覽器會執行如下步驟:】
設置microtask檢查點標誌爲true。
當事件循環microtask執行不爲空時:選擇一個最早進入的microtask隊列的microtask,將事件循環的microtask設置爲已選擇的microtask,運行microtask,將已經執行完成的microtask爲null,移出microtask中的microtask。
清理IndexDB事務
設置進入microtask檢查點的標誌爲false。
重點
總結以上規則爲一條通俗好理解的:
[總結]全部的異步都是爲了按照必定的規則轉換爲同步方式執行。
以上是本人蔘考如下資料後的理解,若是有錯誤的地方,請各位大牛幫忙糾正,謝謝。
JavaScript 運行機制詳解:再談Event Loop