1. 程式人生 > >JS非同步事件原理

JS非同步事件原理

JS非同步原理(事件,佇列)


呼叫棧

  • JS執行時會形成呼叫棧,呼叫一個函式時,返回地址、引數、本地變數都會被推入棧中,如果當前正在執行的函式中呼叫另外一個函式,則該函式相關內容也會被推入棧頂.該函式執行完畢,則會被彈出呼叫棧.變數也隨之彈出,由於複雜型別值存放於堆中,因此彈出的只是指標,他們的值依然在堆中,由GC決定回收.

  • 尾呼叫:指某個函式的最後一步是呼叫另一個函式。由呼叫棧可知,呼叫棧中有a函式,如果a函式呼叫b函式,則b函式也隨之入棧,此時棧中就會有兩個函式.但是如果b函式是a函式最後一步,並且不需保留外層函式呼叫記錄,即a函式呼叫位置變數等都不需要用到,則該呼叫棧中會只保留b函式,這就叫做"尾呼叫優化"(Tail call optimization),即只保留內層函式的呼叫記錄。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫記錄只有一項,這將大大節省記憶體。這就是"尾呼叫優化"的意義。

        function a() {
          let m = 1;
          let n = 2;
          return b(m + n);
        }
        a();
        
        // 等同於
        function a() {
          return b(3);
        }
        a();
        
        // 等同於
        b(3);

事件迴圈(event loop)和任務佇列(task queue)

  • JS的非同步機制由事件迴圈和任務佇列構成.JS本身是單執行緒語言,所謂非同步依賴於瀏覽器或者作業系統等完成. JavaScript 主執行緒擁有一個執行棧以及一個任務佇列,主執行緒會依次執行程式碼,當遇到函式時,會先將函式入棧,函式執行完畢後再將該函數出棧,直到所有程式碼執行完畢。

  • 遇到非同步操作(例如:setTimeout, AJAX)時,非同步操作會由瀏覽器(OS)執行,瀏覽器會在這些任務完成後,將事先定義的回撥函式推入主執行緒的任務佇列(task queue)中,當主執行緒的執行棧清空之後會讀取task queue中的回撥函式,當task queue被讀取完畢之後,主執行緒接著執行,從而進入一個無限的迴圈,這就是事件迴圈.

However, we only have one main thread and one call-stack, so in case there is another request being served when the said file is read, its callback will need to wait for the stack to become empty. The limbo where callbacks are waiting for their turn to be executed is called the task queue (or event queue, or message queue). Callbacks are being called in an infinite loop whenever the main thread has finished its previous task, hence the name 'event loop'.

Microtask 與 Macrotask

  • 一個瀏覽器環境(unit of related similar-origin browsing contexts.)只能有一個事件迴圈(Event loop),而一個事件迴圈可以多個任務佇列(Task queue),每個任務都有一個任務源(Task source)。例如,客戶端可能實現了一個包含滑鼠鍵盤事件的任務佇列,還有其他的任務佇列,而給滑鼠鍵盤事件的任務佇列更高優先順序,例如75%的可能性執行它。這樣就能保證流暢的互動性,而且別的任務也能執行到了。但是,同一個任務佇列中的任務必須按先進先出的順序執行。多個任務佇列,是為了方便控制優先順序。任務佇列是一個先進先出的佇列.

  • macrotask 和 microtask 是非同步任務的兩種分類。在掛起任務時,JS 引擎會將所有任務按照類別分到這兩個佇列中,首先在 macrotask 的佇列(這個佇列也被叫做 task queue)中取出第一個任務,執行完畢後取出 microtask 佇列中的所有任務順序執行;之後再取 macrotask 任務,周而復始,直至兩個佇列的任務都取完。

  • 全部程式碼(script)是一個macrotask,js先執行一個macrotask,執行過程中遇到(setTimeout, setInterval, setImmediate等)非同步操作則建立一個macrotask,遇到(process.nextTick, Promises等)建立一個microtask,這兩個queue分別被掛起.執行棧為空時開始處理macrotask,完成後處理microtask,直到該microtask全部執行完,然後繼續主執行緒呼叫棧.

注:每一次事件迴圈(one cycle of the event loop),只處理一個 (macro)task。待該 macrotask 完成後,所有的 microtask 會在同一次迴圈中處理。處理這些 microtask 時,還可以將更多的 microtask 入隊,它們會一一執行,直到整個 microtask 佇列處理完。

兩個類別的具體分類如下:

macro-task: script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering
micro-task: process.nextTick, Promises(這裡指瀏覽器實現的原生 Promise), Object.observe, MutationObserver