1. 程式人生 > 實用技巧 >javascript事件迴圈機制

javascript事件迴圈機制

一、前言

首先,我們知道javascript是一門單執行緒非阻塞的指令碼語言,這是由其最初的用途來決定的:與瀏覽器互動。

單執行緒則意味著,javascript程式碼在執行時,都只有一個主執行緒來處理所有的任務。而非阻塞則是當代碼需要進行一項非同步任務時(無法立即返回結果,需要花一定時間才能返回的任務,如I/O事件),主執行緒會掛起這個任務,然後在非同步任務返回結果之後再根據一定規則去執行相應的回撥。

單執行緒的必要性:瀏覽器是javascript最主要的執行環境,在其中我們需要進行各種各樣的dom操作。如果javascript是多執行緒的,那麼當兩個執行緒同時對同一個dom進項操作,比如一個向其新增事件,而另一個則刪除了這個dom,此時該如何處理呢?因此,為了保證不發生類似情況,javascript選擇只用一個主執行緒來執行程式碼,從而保證了程式的一致性。

而另一個特性非阻塞,就是通過事件迴圈機制實現的(Event Loop)。事件迴圈機制雖然在瀏覽器和node中存在相似的部分,但兩者間還是有很多的區別,所以分開討論

二、預備知識

2.1、同步和非同步

(1)同步
在函式呼叫時,呼叫者能夠馬上得到結果,那麼就是同步的。
(2)非同步
無法立即返回結果,需要花一定時間才能返回的任務。常見的非同步程序有:

  • DOM事件,由瀏覽器的DOM模組處理,達到觸發條件時,在任務佇列中新增相應的回撥函式
  • setTimeout定時器等,由瀏覽器的timer模組處理,達到設定的時間點時,在任務佇列中添加回調
  • ajax請求,由瀏覽器的NetWork模組處理,等待請求返回後,在任務佇列中添加回調

2.2、執行棧

棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算

由上可知,棧是一種先進後出的資料結構,我們通過執行以下程式碼,來模擬它的入棧和出棧過程。

function fun2() {
    console.log('fun2')
}
function fun1() {
    fun2();
}
setTimeout(() => {
	console.log('setTimeout')
})
fun1();

2.3、任務佇列

以上2.2中展示的是同步程式碼的執行,當js引擎遇到一個非同步事件後並不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務。
當非同步事件返回結果後,js會將這個事件加入不同於當前執行棧的另一個佇列,稱之為任務佇列。事件佇列中的任務,需要等待當前執行棧中的所有任務都執行完畢,主執行緒處於閒置狀態時,主執行緒才會去查詢任務佇列中是否有任務。如果有,再根據一定規則執行,執行的時候同樣也是先執行其中的同步程式碼,如此反覆,就形成了‘事件迴圈’。

任務佇列分為巨集任務佇列和微任務佇列,當非同步事件返回結果後,就會根據這個非同步事件的型別,放到對應的巨集任務佇列或者微任務佇列中。等主執行緒閒置當前執行棧為空時,會優先檢視微任務佇列是否存在任務。如果不存在,再去檢視巨集任務佇列,取出一個事件並把對應的回撥加入到當前執行棧;如果存在,則會依次執行微任務佇列中事件對應的回撥,直到微任務佇列為空,再去檢視巨集任務佇列,取出一個事件執行對應回撥。如此反覆迴圈。

js中存在多個任務佇列,並且不同的任務佇列之間優先順序不同,優先順序高的先被執行,同一佇列中按佇列順序被執行。任務佇列分兩種型別:

  • macro task queue(巨集任務佇列):script(整體程式碼),setTimeout,setInterval,setImmediate,I/O,頁面渲染
  • micro task queue(微任務佇列):process.nextTick,new Promise,new MutaionObserver()

三、事件迴圈機制

總結以上:
(1)先按順序從上到下執行當前全域性上下文
(2)遇到非同步事件就把它交給對應的瀏覽器模組
(3)瀏覽器模組處理完返回結果後,巨集任務放入巨集任務佇列,微任務放入微任務佇列
(4)當主程序執行棧任務清空後,開始執行任務佇列。先執行微任務佇列,執行完微任務佇列後再執行巨集任務佇列
(5)在執行任務佇列時,重複以上步驟。如果遇到非同步程序,相當於再開了一個巨集任務佇列和微任務佇列,按照如上步驟執行,直到當前任務佇列清空,再去執行新的微任務佇列。一直迴圈下去,直到任務佇列清空。

四、例題

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' )  


分析:
(1)首先執行同步程式碼,console.log( ‘script start’ )
(2)遇到setTimeout,會被推入巨集任務佇列
(3)執行async1(), 它也是同步的,只是返回值是Promise,在內部首先執行console.log( ‘async1 start’ )
(4)然後執行async2(), 然後會列印console.log( ‘async2’ )
(5)從右到左會執行, 當遇到await的時候,阻塞後面的程式碼,去外部執行同步程式碼
(6)進入 new Promise,列印console.log( ‘promise1’ )
(7)將.then放入事件迴圈的微任務佇列
(8)繼續執行,列印console.log( ‘script end’ )
(9)外部同步程式碼執行完畢,接著回到async1()內部, 由於async2()其實是返回一個Promise, await async2()相當於獲取它的值,其實就相當於這段程式碼Promise.resolve(undefined).then((undefined) => {}),所以.then會被推入微任務佇列, 所以現在微任務佇列會有兩個任務。接下來處理微任務佇列,列印console.log( ‘promise2’ ),後面一個.then不會有任何列印,但是會
(10)執行後面的程式碼, 列印console.log( ‘async1 end’ )
(11)進入第二次事件迴圈,執行巨集任務佇列, 列印console.log( ‘setTimeout’ )

當我們在函式前使用async的時候,使得該函式返回一個Promise物件。當使用await時,會從右往左執行,當遇到await時,會阻塞函式內部處於它後面的程式碼,去執行該函式外部的同步程式碼,當外部同步程式碼執行完畢,再回到該函式內部執行剩餘的程式碼, 並且當await執行完畢之後,會先處理微任務佇列的程式碼

參考:
https://blog.csdn.net/weixin_39256211/article/details/88855627
https://zhuanlan.zhihu.com/p/33058983 (詳解瀏覽器和node不同環境下js引擎的事件迴圈機制)
https://blog.csdn.net/Jermyo/article/details/103237796 (例題很多,巨集任務有頁面渲染的例子)