Event Loop 是什麼

淺談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的一個練習,有哪些問題歡迎指正。

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