Event loop 機制簡介

堆、棧、隊列

  1. 堆通常是一個可以被看做一棵樹的數組對象。堆總是滿足下列性質:

    • 堆中某個節點的值總是不大於或不小於其父節點的值;
    • 堆總是一棵完全二叉樹。將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。常見的堆有二叉堆、斐波那契堆等。
  2. 堆是在程序運行時,而不是在程序編譯時,申請某個大小的內存空間。即動態分配內存,對其訪問和對一般內存的訪問沒有區別。

  3. 堆是應用程序在運行的時候請求操作系統分配給自己內存,一般是申請/給予的過程。

  4. 堆是指程序運行時申請的動態內存,而棧只是指一種使用堆的方法(即先進後出)。

  1. 棧(stack)又名堆棧,一個數據集合,可以理解爲只能在一端進行插入或刪除操作的列表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱爲棧頂,相對地,把另一端稱爲棧底。
  2. 棧就是一個桶,後放進去的先拿出來,它下面本來有的東西要等它出來之後才能出來(先進後出),對應js數組操作裏的push(入棧)pop(出棧)
  3. 棧(Stack)是操作系統在建立某個進程時或者線程(在支持多線程的操作系統中是線程)爲這個線程建立的存儲區域,該區域具有FIFO的特性,在編譯的時候可以指定需要的Stack的大小。

隊列

是一種支持先進先出(FIFO)的集合,即先被插入的數據,先被取出!

執行棧

當javascript代碼執行的時候會將不同的變量存於內存中的不同位置:堆(heap)和棧(stack)中來加以區分。其中,堆裏存放着一些對象。而棧中則存放着一些基礎類型變量以及對象的指針。 但是我們這裏說的執行棧和上面這個棧的意義卻有些不同。
js 在執行可執行的腳本時,首先會創建一個全局可執行上下文globalContext,每當執行到一個函數調用時都會創建一個可執行上下文(execution context)EC。當然可執行程序可能會存在很多函數調用,那麼就會創建很多EC,所以 JavaScript 引擎創建了執行上下文棧(Execution context stack,ECS)來管理執行上下文。當函數調用完成,js會退出這個執行環境並把這個執行環境銷燬,回到上一個方法的執行環境... 這個過程反覆進行,直到執行棧中的代碼全部執行完畢:

下面來看個簡單的例子:

function fun3() {
    console.log('fun3')
}
function fun2() {
    fun3();
}
function fun1() {
    fun2();
}
fun1();

當執行一個函數的時候,就會創建一個執行上下文,並且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。知道了這樣的工作原理,讓我們來看看如何處理上面這段代碼:

1.執行全局代碼,創建全局執行上下文,全局上下文被壓入執行上下文棧

ECStack = [
    globalContext
];

2.全局上下文初始化

   globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

3.初始化的同時,fun1 函數被創建,保存作用域鏈到函數的內部屬性[[scope]]

    fun1.[[scope]] = [
      globalContext.VO
    ];

4.執行 fun1 函數,創建 fun1 函數執行上下文,fun1 函數執行上下文被壓入執行上下文棧

    ECStack = [
        fun1,
        globalContext
    ];

5.fun1函數執行上下文初始化:

  1. 複製函數 [[scope]] 屬性創建作用域鏈,
  2. 用 arguments 創建活動對象,
  3. 初始化活動對象,即加入形參、函數聲明、變量聲明,
  4. 將活動對象壓入fun1 作用域鏈頂端。
    同時 f 函數被創建,保存作用域鏈到 f 函數的內部屬性[[scope]]
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
  1. 執行 fun2()函數,重複步驟2。
  2. 最終形成這樣的執行棧:
    ECStack = [
        fun3
        fun2,
        fun1,
        globalContext
    ];

8.fun3執行完畢,從執行棧中彈出...一直到fun1

事件隊列

以上的過程說的都是同步代碼的執行。那麼當一個異步代碼(如發送ajax請求數據)執行後會如何呢?接下來需要了解的另一個概念就是:事件隊列(Task Queue)。
當js引擎遇到一個異步事件後,其實不會說一直等到異步事件的返回,而是先將異步事件進行掛起。等到異步事件執行完畢後,會被加入到事件隊列中。(注意,此時只是異步事件執行完成,其中的回調函數並沒有去執行。)當執行隊列執行完畢,主線程處於閒置狀態時,會去異步隊列那抽取最先被推入隊列中的異步事件,放入執行棧中,執行其中的回調同步代碼。如此反覆,這樣就形成了一個無限的循環。這就是這個過程被稱爲“事件循環(Event Loop)”的原因。
爲了更好的理解,我們來看一張圖:(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)

主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

macro task與micro task

在介紹之前,我們先看一段經典的代碼執行:

setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})

會看到控制檯先後分別輸出:2、3、1。
先看一下阮老師對setTimeout的一些解釋:

setTimeout(fn,0)的含義是,指定某個任務在主線程最早可得的空閒時間執行,也就是說,儘可能早得執行。它在"任務隊列"的尾部添加一個事件,因此要等到同步任務和"任務隊列"現有的事件都處理完,纔會得到執行。需要注意的是,setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等很久,所以並沒有辦法保證,回調函數一定會在setTimeout()指定的時間執行。
實際上,一般因爲異步任務之間並不相同,因此他們的執行優先級也有區別。不同的異步任務被分爲兩類:微任務(micro task)和宏任務(macro task)。

以下事件屬於宏任務:

  • setTimeout
  • MessageChannel
  • postMessage
  • setImmediate

以下事件屬於微任務

  • new Promise()
  • new MutaionObserver()

前面我們介紹過,在一個事件循環中,異步事件返回結果後會被放到一個任務隊列中。然而,根據這個異步事件的類型,這個事件實際上會被對應的宏任務隊列或者微任務隊列中去。並且在當前執行棧爲空的時候,主線程會 查看微任務隊列是否有事件存在。如果不存在,那麼再去宏任務隊列中取出一個事件並把對應的回到加入當前執行棧;如果存在,則會依次執行隊列中事件對應的回調,直到微任務隊列爲空,然後去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧...如此反覆,進入循環。

我們只需記住噹噹前執行棧執行完畢時會立刻先處理所有微任務隊列中的事件,然後再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務之前執行。
所以我們就很好解釋上面的那段代碼了。

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