淺談Event Loop
從單線程說起
衆所周知,js是一種單線程語言。爲什麼是單線程呢?我引用一句爛大街的話:假設js同時有兩個線程,一個線程想要在某個dom節點上增加內容,另一個線程想要刪除這個節點,這時要以哪個爲準呢?當然,多線程有多線程的解決辦法,加鎖啊,但是這樣的話,又會引入鎖、狀態同步等問題。
js是瀏覽器腳本語言,主要用途是與用戶互動,操作dom,多線程會帶來很複雜的同步問題。
好吧,那就單線程吧。但是單線程又帶來了單線程的問題,只有一個線程啊,任務要排隊執行,如果前一個任務執行時間很長(ajax請求後臺數據),後面的任務就都得等着。
Event Loop就出現了,來背單線程的鍋。
任務隊列
單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
如果排隊是因爲計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒着的,因爲IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等着結果出來,再往下執行。
JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。
於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
宏任務&微任務
整理了一下常見了微任務、宏任務
- 常見的宏任務:setTimeout、setInterval、I/O、setImmedidate
- 常見的微任務:process.nextTick、MutationObserver、Promise.then、 catch finally、ajax請求
process.nextTick和setImmidate是隻支持Node環境的。且process.nextTick是有一個插隊操作的,就是說他進入微任務隊列時,會插到除了process.nextTick 其他的微任務前面。
所以,我們上面提到的任務隊列,是包括一個宏任務隊列和一個微任務隊列的。每次執行棧爲空的時候,系統會優先處理微任務隊列,處理完微任務隊列裏的所有任務,再去處理宏任務。
Event Loop
往下看之前你應該知道棧、隊列、同步任務、異步任務(宏任務&微任務)、執行棧這些基本概念。
請看下圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-utRj5RTq-1588226339527)(images/event-loop.png)]
1、js在執行代碼時,代碼首先進入執行棧,代碼中可能包含一些同步任務和異步任務。同步任務都在主線程(這裏的主線程就是JS引擎線程)上執行。同步任務立即執行,執行完出棧,over。
2、異步任務會再分爲宏任務和微任務。微任務會進入到另一個Event Table中,並在裏面註冊回調函數,每當指定的事件完成時,Event Table會將這個函數移到Event Queue中。宏任務也會進入到Event Table中,並在裏面註冊回調函數,每當指定的事件完成時,Event Table會將這個函數移到Event Queue中。
3、當主線程內的任務執行完畢,主線程爲空時,會檢查微任務的Event Queue,如果有任務,就全部執行,如果沒有就執行下一個宏任務。
以上三步會不斷重複,這就是事件循環(Event Loop)。
demo
來看一個簡單的demo。
setTimeout(function() {
console.log('1');
})
new Promise(function(resolve) {
resolve()
console.log('2');
}).then(function() {
console.log('3');
})
console.log('4');
//打印順序 2 4 3 1
上面demo的圖解
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hgUHzlJg-1588226339546)(images/demo1.png)]
再看一個demo
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');
// 打印順序是:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
看到async/await不必緊張,語法糖而已。async表示函數裏有異步操作,await之前的代碼該怎麼執行怎麼執行,await右側表達式照常執行,後面的代碼被阻塞掉,等待await的返回。返回是非promise對象時,執行後面的代碼;返回promise對象時,等promise對象resolved時再執行。
所以可以理解成後面的代碼放到了promise.then 裏面。
- 輸出 script start
- 之後把setTimeout裏面的匿名回調函數丟進宏任務隊列,簡記爲[‘setTimeout’]
- 輸出async1 start
- 輸出async2
- 要輸出async1 end代碼被丟進微任務隊列,此時的微任務隊列爲[‘async1 end’]
- 輸出promise1
- promise對象狀態變爲resolved
- promise.then 裏的匿名函數進入微任務隊列,此時的微任務隊列爲[‘async1 end’, ‘promise2’]
- 輸出script end
- 執行棧空
- 輸出async1 end
- 輸出promise2
- 微任務隊列爲空
- 輸出setTimeout
以上兩個demo就是對Event Loop的一個練習,有哪些問題歡迎指正。