目錄 為什麼分析asap asap概述 asap源碼解析—Node版 參考 1.為什麼分析asap 在之前的文章 "async和await是如何實現非同步編程?" 中的 “淺談Promise如何實現非同步執行” 小節,提到了 Promise 非同步執行是通過 "asap" 這個庫來實現的。所以為了進一步深 ...
目錄
- 為什麼分析asap
- asap概述
- asap源碼解析—Node版
- 參考
1.為什麼分析asap
在之前的文章 async和await是如何實現非同步編程? 中的 “淺談Promise如何實現非同步執行” 小節,提到了 Promise 非同步執行是通過 asap 這個庫來實現的。所以為了進一步深入 Promise 非同步執行的原理,深入分析一下 asap 是有必要的。
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.參考
asap - High-priority task queue for Node.js and browsers