由於js設計之初,多線程的執行模式還不流行,因此一直覺得,js都是單線程執行的。可是js擁有異步執行的能力,這依賴於事件循環(Event Loop)的執行模式。咱們將經過js在瀏覽器中的執行來研究一下該模式。javascript
其中涉及到一些概念,咱們先簡單研究一下,以便後續更好地瞭解。java
參考阮一峯的解釋,將整個CPU比喻爲一座工廠,進程就是其中的車間,車間中的須要完成的工序就是線程。一個工廠能夠有多個車間,每一個車間有一個或者多個工序,可是必須 按照順序執行,這就是單線程的概念。也是瀏覽器事件執行的基礎。ajax
瀏覽器是一個多進程應用,每個窗口就是一個進程,其中包含如下線程:promise
負責渲染頁面,佈局和繪製瀏覽器
頁面須要重繪和迴流時,該線程就會執行網絡
與js引擎線程互斥,防止渲染結果不可預期數據結構
負責處理解析和執行javascript腳本程序多線程
只有一個JS引擎線程(單線程)異步
與GUI渲染線程互斥,防止渲染結果不可預期async
用來控制事件循環(鼠標點擊、setTimeout、ajax等)
當事件知足觸發條件時,將事件放入到JS引擎所在的執行隊列中
setInterval與setTimeout所在的線程
定時任務並非由JS引擎計時的,是由定時觸發線程來計時的
計時完畢後,通知事件觸發線程
瀏覽器有一個單獨的線程用於處理AJAX請求
當請求完成時,如有回調函數,通知事件觸發線程
同步任務都在js引擎線程上完成,當前的任務都存儲在執行棧中;
js引擎線程執行到setTimeout/setInterval
的時候,通知定時觸發器線程,間隔必定時間,觸發回調函數;
定時觸發器線程在接收到這個消息後,會在等待的時間後,將回調事件放入到由事件觸發線程所管理的事件隊列(事件隊列分爲宏任務隊列和微任務隊列)中;
js引擎線程執行到XHR/fetch時
,通知 異步http請求線程,發送一個網絡請求;
異步http請求線程在請求成功後,將回調事件放入到由事件觸發線程的事件隊列中;
若是JS引擎線程中的執行棧沒有任務了,JS引擎線程會詢問事件觸發線程,在 事件隊列中是否有待執行的回調函數,若是有就會加入到執行棧中交給JS引擎線程執行;
JS引擎線程空閒以後,GUI渲染線程開始工做
JS 是能夠操做 DOM 的, 所以瀏覽器設定 GUI渲染線程和 JS引擎線程爲互斥關係;
setTimeout/setInterval
和 XHR/fetch
代碼執行時, 自己是同步任務,而其中的回調函數纔是異步任務
JS引擎線程只執行執行棧中的事件
執行棧中的代碼執行完畢,就會讀取事件隊列中的事件
事件隊列中的回調事件,是由各自線程插入到事件隊列中的
如此循環
瞭解了瀏覽器多線程之間的關聯以後,咱們開始探究,js是如何依賴Event Loop,進行異步操做的。
在分析多線程之間的關係時,咱們提到了兩個概念,執行棧和執行隊列
棧,是一種數據結構,具備先進後出的原則。JS 中的執行棧就具備這樣的結構,當引擎第一次遇到 JS 代碼時,會產生一個全局執行上下文並壓入執行棧,每遇到一個函數調用,就會往棧中壓入一個新的上下文。引擎執行棧頂的函數,執行完畢,彈出當前執行上下文
事件隊列是一個存儲着 異步任務 的隊列,按照先進先出的原則執行。事件隊列每次僅執行一個任務。當執行棧爲空時,JS 引擎便檢查事件隊列,若是事件隊列不爲空的話,事件隊列便將第一個任務壓入執行棧中運行。
異步任務又分爲宏任務跟微任務、他們之間的區別主要是執行順序的不一樣。
也叫tasks,一些異步任務的回調會依次進入macro task queue
,等待後續被調用,這些異步任務包括:
也叫jobs,另外一些異步任務的回調會依次進入micro task queue
,等待後續被調用,這些異步任務包括:
下面上一道很經典的題目:
console.log('1');
setTimeout(()=>{
console.log('2');
},100);
setTimeout(()=>{
console.log('3');
},0);
console.log('4');
複製代碼
沒有研究event loop以前,答案極可能覺得是1 3 4 2
,可是實際答案是1 4 3 2
。其中的原理下面來分析一下。
異步任務執行的時候,有這樣一個順序:
microtask queue
中取出位於隊首的回調任務,放入調用棧Stack中執行*注:
以上就是瀏覽器事件循環——event loop。
理解了異步任務的執行順序以後,再來回顧上面這道題:
console.log('1');
setTimeout(()=>{
console.log('2');
},100);
setTimeout(()=>{
console.log('3');
},0);
console.log('4');
複製代碼
console.log('1')
是同步任務,setTimeout
是宏任務,js引擎線程通知事件觸發線程,在定時n秒後存入宏任務隊列中,因此先存入console.log('3')
,後存入console.log('2');
;console.log('4');
;console.log('3');
,後執行console.log('2');
1 4 3 2
;再來2道題鞏固一下:
一.
setTimeout(() => {
console.log('A');
}, 0);
var obj = {
func: function() {
setTimeout(function() {
console.log('B');
}, 0);
return new Promise(function(resolve) {
console.log('C');
resolve();
});
},
};
obj.func().then(function() {
console.log('D');
});
console.log('E');
複製代碼
setTimeout
放到宏任務隊列,此時宏任務隊列爲['A']
;obj
的func
方法,將setTimeout
放到宏任務隊列,此時宏任務隊列爲['A', 'B']
Promise
,由於這是一個同步操做,因此先打印出'C'
;then
放到微任務隊列,此時微任務隊列爲 ['D']
;console.log('E');
,打印出 'E'
;['A', 'B']
;C E D A B
二.
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
複製代碼
await
前面的代碼是同步的,調用此函數時會直接執行;而await a();
這句能夠被轉換成 Promise.resolve(a());
await
後面的代碼 則會被放到 Promise.then()
方法裏。所以上面的代碼能夠被轉換成以下形式:
function async1() {
console.log('async1 start'); // 2
Promise.resolve(async2()).then(() => {
console.log('async1 end'); // 6
});
}
function async2() {
console.log('async2'); // 3
}
console.log('script start'); // 1
setTimeout(function() {
console.log('settimeout'); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); // 7
});
console.log('script end'); // 5
複製代碼
script start
settimeout
添加到宏任務隊列,此時宏任務隊列爲['settimeout']
async1
,先打印出async1 start
,又由於Promise.resolve(async2())
是同步任務,因此打印出async2
,接着將async1 end
添加到微任務隊列,此時微任務隊列爲['async1 end']
promise1
,將promise2
添加到微任務隊列,此時微任務隊列爲['async1 end', promise2]
script end
async1 end
和promise2
settimeout