asap異步執行實現原理


目錄

  • 為什么分析asap
  • asap概述
  • asap源碼解析—Node版
  • 參考

1.為什么分析asap

在之前的文章 async和await是如何實現異步編程? 中的 “淺談Promise如何實現異步執行” 小節,提到了 Promise 異步執行是通過 asap 這個庫來實現的。所以為了進一步深入 Promise 異步執行的原理,深入分析一下 asap 是有必要的。
補充說明:這里提及的Promise並不是Node和瀏覽器的原生實現,是一個第三方庫實現,僅以此為參考。


2.asap概述

asap 是 as soon as possible 的簡稱,在 Node 和瀏覽器環境下,能將回調函數以高優先級任務來執行(下一個事件循環之前),即把任務放在微任務隊列中執行。

宏任務(macro-task)和微任務(micro-task)表示異步任務的兩種分類。在掛起任務時,JS 引擎會將所有任務按照類別分到這兩個隊列中,首先在 macrotask 的隊列(這個隊列也被叫做 task queue)中取出第一個任務,執行完畢后取出 microtask 隊列中的所有任務順序執行;之后再取 macrotask 任務,周而復始,直至兩個隊列的任務都取完。

用法:

asap(function () {
    // ...
});

3.asap源碼解析—Node版

asap 源碼庫中包含了支持Node和瀏覽器的兩個版本,這里主要進行分析Node版。

主要包含兩個源碼文件:

這兩個文件分別導出了 asap 和 rawAsap 這兩個方法,而 asap 可以看作是對 rawAsap 的進一步封裝,通過緩存的 domain(可以捕捉處理 try catch 無法捕捉的異常,針對異步代碼的異常處理)和 try/finally 實現了即使某個任務拋出異常也可以恢復任務棧的繼續執行,另外也做了一點緩存優化(具體見源碼)。

因此這里主要分析 raw.js 里面的代碼即可:

1.首先是對外導出的 rawAsap 方法

var queue = [];
var flushing = false;

function rawAsap(task) {
    if (!queue.length) {
        requestFlush();
        flushing = true;
    }
    queue[queue.length] = task;
}

源碼解析:如果任務棧 queue 為空,則觸發 requestFlush 方法,並將 flushing 標志為 true,並且始終會將要執行的 task 添加到任務棧 queue 的末尾。這里需要注意的是由於 requestFlush 中是異步去觸發任務棧的執行的,所以即使queue[queue.length] = task在 requestFlush 調用之后執行,也能保證在任務棧 queue 真正執行前,任務 task 已經被添加到了任務棧 queue 的末尾。(如果任務棧 queue 不為空,說明 requestFlush 已經觸發了,此時任務棧正在被循環依次執行,執行完畢會清空任務棧)

2.其次是異步觸發 flush 方法執行的 requestFlush 方法

var domain; 
var hasSetImmediate = typeof setImmediate === "function";

// 設置為 rawAsap 的屬性,方便在任務執行異常時再次觸發 requestFlush
rawAsap.requestFlush = requestFlush;
function requestFlush() {
    // 確保 flushing 未綁定到任何域
    var parentDomain = process.domain;
    if (parentDomain) {
        if (!domain) {
            // 惰性加載執行 domain 模塊
            domain = require("domain");
        }
        domain.active = process.domain = null;
    }
  
    if (flushing && hasSetImmediate) {
        setImmediate(flush);
    } else {
        process.nextTick(flush);
    }

    if (parentDomain) {
        domain.active = process.domain = parentDomain;
    }
}

源碼解析:核心代碼其實就一句:setImmediate(flush),通過 setImmediate 異步執行 flush 方法。而判斷 parentDomain 以及設置和恢復 domain 都只是為了當前的 flush 方法不綁定任何域執行。而這里還有一個 hasSetImmediate 判斷,是為了做兼容降級處理,如果不存在 setImmediate 方法,則使用 process.nextTick 方法觸發異步執行。但使用 process.nextTick 方法有一個缺陷,就是它不能夠處理遞歸。

3.最后是執行任務棧的 flush 方法

// 下一個任務在任務隊列中執行的位置
var index = 0;
var capacity = 1024;

function flush() {
    while (index < queue.length) {
        var currentIndex = index;
        // 在調用任務之前先設置下一個任務的索引,可以確保再次觸發 flush 方法時,跳過異常任務
        index = index + 1;
        queue[currentIndex].call();

        // 防止內存泄露
        if (index > capacity) {
            for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) {
                queue[scan] = queue[scan + index];
            }
            queue.length -= index;
            index = 0;
        }
    }
    queue.length = 0;
    index = 0;
    flushing = false;
}

源碼解析:通過 while 循環依次去執行任務棧 queue 中的每一個任務,這里需要注意一點,index + 1 表示下一個要執行的任務下標,而其放在 queue[currentIndex].call() 之前,是為了保證當當前任務執行發生異常了,再次觸發 requestFlush 方法時,能夠跳過發生異常的任務,從下一個任務開始執行。而判斷 if (index > capacity) 是為了防止內存泄露,當任務棧 queue 的長度超過了指定的閾值 capacity 時,對任務棧 queue 中的任務進行移動,將所有剩余的未執行的任務置前,並重置任務棧 queue 的長度。當所有任務執行完畢后,重置任務棧以及相應狀態。

4.總結

rawAsap 方法是通過 setImmediate 或 process.nextTick 來實現異步執行的任務棧,而 asap 方法是對 rawAsap 方法的進一步封裝,通過緩存的 domain 和 try/finally 實現了即使某個任務拋出異常也可以恢復任務棧的繼續執行(再次調用rawAsap.requestFlush)。


4.參考

【翻譯】Promises/A+規范

asap - High-priority task queue for Node.js and browsers


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM