淺談Event Loop

衆所周知,js是一種單線程語言。爲什麼是單線程呢?我引用一句爛大街的話:假設js同時有兩個線程,一個線程想要在某個dom節點上增加內容,另一個線程想要刪除這個節點,這時要以哪個爲準呢?當然,多線程有多線程的解決辦法,加鎖啊,但是這樣的話,又會引入鎖、狀態同步等問題。
js是瀏覽器腳本語言,主要用途是與用戶互動,操作dom,多線程會帶來很複雜的同步問題。
好吧,那就單線程吧。但是單線程又帶來了單線程的問題,只有一個線程啊,任務要排隊執行,如果前一個任務執行時間很長(ajax請求後臺數據),後面的任務就都得等着。
Event Loop就出現了,來背單線程的鍋。
Event Loop
往下看之前你應該知道棧、隊列、同步任務、異步任務、執行棧這些基本概念。
關於執行棧有一篇很詳細的文章推薦:JavaScript深入之執行上下文棧
請看下圖:

js在執行代碼時,代碼首先進入執行棧,代碼中可能包含一些同步任務和異步任務。

同步任務立即執行,執行完出棧,over。

異步任務也就是常見的ajax請求、setTimeout等,代碼調用到這些api的時候,WebAPIs來處理這些問題,執行棧繼續執行。

異步任務有了運行結果時,(當ajax請求結果返回時),WebAPIs把對應的回調函數放到任務隊列。

執行棧爲空時來讀取任務隊列中的第一個函數,壓入執行棧。

步驟5不斷重複,執行棧爲空時,系統就去任務隊列中拿第一個函數壓入棧繼續執行。這個過程不斷重複,這就是事件循環(Event Loop)。
來看一個簡單的demo。
console.log(1);

setTimeout(() => {
console.log(2);
}, 2000);

console.log(3);
複製代碼
console.log(1) 同步任務,輸出1
setTimeout異步任務,交給webapis去處理,2s後,console.log(2)進入任務隊列
console.log(3)同步任務,輸出3
執行棧爲空,系統讀取任務隊列裏的事件
執行console,log(2),輸出2

宏任務&微任務
說到這兒當然還沒完。相信你肯定見過process.nextTick、promise吧,這時候執行順序會有點兒複雜,往下看。
微任務、宏任務與Event-Loop用了很通俗的例子講了宏任務和微任務的區別,我這裏就不囉嗦了。如果你不想了解也沒關係,因爲常見的宏任務、微任務就那幾種,記住就可以了。

常見的宏任務:script(整體代碼)、setTimeout、setInterval、I/O、setImmedidate
常見的微任務:process.nextTick、MutationObserver、Promise.then catch finally

process.nextTick和setImmidate是隻支持Node環境的。
還有,process.nextTick是有一個插隊操作的,就是說他進入微任務隊列時,會插到除了process.nextTick 其他的微任務前面。
所以,我們上面提到的任務隊列,是包括一個宏任務隊列和一個微任務隊列的。每次執行棧爲空的時候,系統會優先處理微任務隊列,處理完微任務隊列裏的所有任務,再去處理宏任務。
做兩道題
前面叨叨了那麼多,下面做兩道題試試水吧。
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4);
}).then(t => console.log(t));
console.log(3);
複製代碼hahahhhhh我搬出了阮老師的題。

首先new Promise執行,resolve(1)表示創建的promise對象的狀態變爲resolved
Promise.resolve()相當於創建了一個promise對象,then裏面的匿名回調函數進入微任務隊列,此時的微任務隊列是[() => console.log(2)]
輸出 4
new Promise的then函數裏面的匿名回調進入微任務隊列, 此時的微任務隊列是[() => console.log(2), t => console.log(t)]
輸出 3

所以,最後輸出的順序是4 3 2 1。
emmmmmmm如果你不懂,那我覺得你可以先去複習一下promise。
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’);
});
process.nextTick(() => {
console.log(‘nextTick’);
})
console.log(‘script end’);
複製代碼這是一道爛大街的面試題。。我不信你沒見過。話不多說,我們來分析emmmmm我建議你自己先做一下,再往下看。
看到async/await不必緊張,語法糖而已。async表示函數裏有異步操作,await之前的代碼該怎麼執行怎麼執行,await右側表達式照常執行,後面的代碼被阻塞掉,等待await的返回。返回是非promise對象時,執行後面的代碼;返回promise對象時,等promise對象resolved時再執行。
所以可以理解成後面的代碼放到了promise.then 裏面。

輸出 script start
WebAPIs在0s(哦,好像最短是4ms)之後把setTimeout裏面的匿名回調函數丟進宏任務隊列,簡記爲[‘setTimeout’] (請記得丟進任務隊列裏的是回調函數,函數!)
輸出async1 start
輸出async2
要輸出async1 end代碼被丟進微任務隊列,此時的微任務隊列爲[‘async1 end’]
輸出promise1
promise對象狀態變爲resolved
promise.then 裏的匿名函數進入微任務隊列,此時的微任務隊列爲[‘async1 end’, ‘promise2’]
nextTick插隊到微任務隊列對首,[‘nextTick’, ‘async1 end’, ‘promise2’]
輸出script end
執行棧空
輸出nextTick
輸出async1 end
輸出promise2
微任務隊列爲空
輸出setTimeout

結語
如果看完本文你還是沒太懂,那我建議你可以多看幾篇文章,一個燒餅吃不飽,十個就差不多了。
涉及到promise、async/await、Node和瀏覽器環境下事件循環的區別等問題本文沒有細講,但是這些知識會幫你更好地掌握Event Loop。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章