圖解JavaScript中的async generator函數執行順序

ES6中的async generator函數很好用,在處理一些列的異步調用管理上很方便。可以方便的處理異步函數,可以用來管理異步的、序列的異步工作流。但是執行的順序確實有點繞,腦子不太好使,端午節期間花了一天的時間來徹底梳理了一下。

注意,看這篇文章前,最好要先了解generator函數的運行機制和基本的使用。這篇文章不做闡述。

直接擼代碼

一、generator函數中,直接使用yield返回

// 模擬一個異步函數
function mockFetch(url, ms){   
    log(`mockFetch enter :: url = ${url}, ms = ${ms}`); // A1行
    //return ms;
    return new Promise((resolve, reject)=>{
        log(`mockFetch new promise enter :: url = ${url}, ms = ${ms}`); // A2行
        setTimeout(()=>{
            // 模擬resove和reject場景
            if(ms % 2 ===0){
                resolve(`mockFetch end :: url = ${url}, ms = ${ms}`); // A3行
            }else{
                reject(new Error("mockFetch :: error, not support!!")) // A4行
            }
        },ms || 600);
    })
}

// 異步生成函數
async function* asyncGen(){
    log(`asyncGen :: enter.`); // B1 行
    yield mockFetch("google.com",800); // B2 行
    yield mockFetch("taobao.com",600); // B3 行
    yield mockFetch("bd.com",100);// B4 行
    return "asyncGen end."; // B5 行
}

// 測試使用異步生成函數
 function testAsyncGen2(){
    log(`testAsyncGen2 :: enter.`); // C1 行
    let ag = asyncGen(); // C2 行
    let rst1 = ag.next();// C3 行
    log(`testAsyncGen2 :: rst1 = ${rst1}.`);// C4 行

    let rst2 = ag.next();// C5 行
    log(`testAsyncGen2 :: rst2 = ${rst2}, ${rst1 ===rst2}`);// C6 行

    let rst3 = ag.next(); // C7 行
    log(`testAsyncGen2 :: rst3 = ${rst3}, ${rst1 ===rst2}, ${rst3 ===rst2}`);// C8 行

    let rst4 = ag.next(); // C9 行
    log(`testAsyncGen2 :: rst4 = ${rst4}.`);// C10 行
}
testAsyncGen2();

// 以下是執行順序,很重要,也是很坑的地方
[16:21:38:788] testAsyncGen2 :: enter.  // C1 行
[16:21:38:796] asyncGen :: enter. //B1
[16:21:38:797] mockFetch enter :: url = google.com, ms = 800 // A1
[16:21:38:797] mockFetch new promise enter :: url = google.com, ms = 800 //A2
[16:21:38:798] testAsyncGen2 :: rst1 = [object Promise]. // C4 行
[16:21:38:798] testAsyncGen2 :: rst2 = [object Promise], false // C6 行
[16:21:38:798] testAsyncGen2 :: rst3 = [object Promise], false, false // C8 行
[16:21:38:798] testAsyncGen2 :: rst4 = [object Promise]. // C10 行
[16:21:39:603] mockFetch enter :: url = taobao.com, ms = 600 // A1
[16:21:39:603] mockFetch new promise enter :: url = taobao.com, ms = 600  //A2
[16:21:40:208] mockFetch enter :: url = bd.com, ms = 100 // A1
[16:21:40:208] mockFetch new promise enter :: url = bd.com, ms = 100  //A2

從上面的執行結果可以看出來:異步生成函數,在執行它的next的時候,是按順序執行完成一個再執行一個的,是串行的執行,並不是並行的執行!!! 這個比較坑,一定要注意。跟進上述測試結果,基本可以推測,異步執行函數的執行器是這樣的(以上述方法爲示例講解):

1)調用異步生成函數的時候,返回的是一個異步迭代器。

2)異步迭代器在執行next方法的時候,方法先返回了一個promise對象,然後開始啓動上述的異步生成器方法:此時會輸出B1行:
[16:21:38:796] asyncGen :: enter.  // B1 行

3)函數執行到B2行,遇到yiled表達式,開始調用mockFetch方法,此時執行器進入mockFetch,執行到A1、A2行:
[16:21:38:797] mockFetch enter :: url = google.com, ms = 800 // A1 行
[16:21:38:797] mockFetch new promise enter :: url = google.com, ms = 800 // A2 行

4)mockFetch函數返回Promise對象(暫時叫mfPromise)給B2行的yiled,此時不會再將mfPromise返回給next。

5)執行器進入C4行輸出日誌,接着進入C5行調用next,注意,注意,此時調用next並沒有真正的進入B3行,而是壓入了一個執行隊列中(有點像事件循環的),因此此時並沒有調用mockFetch函數

6)執行器進入C6行輸出日誌,接着進入C7行調用next,此時調用next也沒有真正的進入B3行,而是壓入了一個執行隊列中,因此此時並沒有調用mockFetch函數

7),因此C5~C10行,順序執行,調用next的地方,都沒有真正的調用,而是壓入了一個執行任務隊列。等待第一個任務執行完成之後再順序執行,因此緊挨着輸出日誌:

[16:21:38:798] testAsyncGen2 :: rst1 = [object Promise]. // C4 行
[16:21:38:798] testAsyncGen2 :: rst2 = [object Promise], false // C6 行
[16:21:38:798] testAsyncGen2 :: rst3 = [object Promise], false, false // C8 行
[16:21:38:798] testAsyncGen2 :: rst4 = [object Promise]. // C10 行

8),800毫秒之後(第一個異步的mock異步時間)mockFetch返回的promise mfPromise 狀態變爲fullfilled,此時纔會將第一個返回的promise rst1的狀態也變爲fullfilled 的,同時準備回調rst1.then【本次示例沒寫then哈】

9),緊接着從任務隊列裏面取出第二次調用next那個任務,開始執行next,此時才進入asyncGen預計的第二個yield部分,開始第二次調用mockFetch,然後進入上述步驟4~8

[16:21:39:603] mockFetch enter :: url = taobao.com, ms = 600 // A1
[16:21:39:603] mockFetch new promise enter :: url = taobao.com, ms = 600  //A2

10),600毫秒之後,第二次mockFetch返回的promise mfPromise 狀態變爲fullfilled,此時會將第二次next返回的promise rst2的狀態也變爲fullfilled 的,同時準備回調rst2.then【本次示例沒寫then哈】

函數的調用時序圖(圖中的9、11、14等步驟是JS解釋引擎處理的):

二、generator函數中,結合await使用

先上代碼:

//爲了方便在node中log出來的時候看時間,簡單找了個以前封裝的日誌方法,就只是在前面加上了時間
let {log} = require("../../util/logger");
// 模擬異步獲取遠程數據
function mockFetch(url, ms){  
    log(`mockFetch enter :: url = ${url}, ms = ${ms}`); 
    return new Promise((resolve, reject)=>{// 對應時序圖的 7
        log(`mockFetch new promise enter :: url = ${url}, ms = ${ms}`);
        setTimeout(()=>{
            // 模擬resove和reject場景
            if(ms % 2 ===0){
                resolve(`mockFetch end :: url = ${url}, ms = ${ms}`);
            }else{
                reject(new Error("mockFetch :: error, not support!!"))
            }
        },ms || 600);
    })
}
// 異步生成函數,注意,這裏yield前面使用了await了,yield的是一個具體的結果了。
async function* asyncGenWithWait(){
    log(`asyncGenWithWait :: enter.`); // 對應時序圖的: 5
    let mockRst1 =  await mockFetch("google.com",800); // 對應時序圖的: 6
    yield "Result mockRst1===>>> "+ mockRst1; // 放到{value:xxx}對象上返回。
    let mockRst2 =  await  mockFetch("taobao.com",600); // 對應時序圖的: 14、15
    yield "Result mockRst2===>>> "+ mockRst2;
    let mockRst3 =  await  mockFetch("bd.com",100);
    yield "Result mockRst3===>>> "+ mockRst3;
    return "asyncGenWithWait end.";
}
 // 測試使用異步生成函數,這裏是否使用async沒關係
 async function testAsyncGenWait(){
    log(`testAsyncGenWait :: enter.`);
    let ag = asyncGenWithWait(); // 對應時序圖的 1. call, 2, 返回異步迭代器ag
    log(`testAsyncGenWait :: called asyncGenWithWait`);
    let rst1 = ag.next(); // 對應時序圖的 3、4
    log(`testAsyncGenWait :: rst1 = ${rst1}.`);
    rst1.then(({value, done})=>{log(`testAsyncGenWait :: rst1, value = ${value}, done = ${done}`)});
    let rst2 = ag.next(); // 對應時序圖的 8
    log(`testAsyncGenWait :: rst2 = ${rst2}, ${rst1 ===rst2}`); // false
    rst2.then(({value, done})=>{log(`testAsyncGenWait :: rst2, value = ${value}, done = ${done}`)});
    let rst3 = ag.next(); // 對應時序圖的 10
    log(`testAsyncGenWait :: rst3 = ${rst3}, ${rst1 ===rst2}, ${rst3 ===rst2}`); // false
    rst3.then(({value, done})=>{log(`testAsyncGenWait :: rst3, value = ${value}, done = ${done}`)});
    let rst4 = ag.next();
    log(`testAsyncGenWait :: rst4 = ${rst4}.`);
    rst4.then(({value, done})=>{log(`testAsyncGenWait :: rst4, value = ${value}, done = ${done}`)});
}
testAsyncGenWait();

執行結果:

[22:43:51:608] testAsyncGenWait :: enter.
[22:43:51:650] testAsyncGenWait :: called asyncGenWithWait
[22:43:51:651] asyncGenWithWait :: enter.
[22:43:51:651] mockFetch enter :: url = google.com, ms = 800
[22:43:51:651] mockFetch new promise enter :: url = google.com, ms = 800
[22:43:51:652] testAsyncGenWait :: rst1 = [object Promise].
[22:43:51:652] testAsyncGenWait :: rst2 = [object Promise], false
[22:43:51:652] testAsyncGenWait :: rst3 = [object Promise], false, false
[22:43:51:652] testAsyncGenWait :: rst4 = [object Promise].
[22:43:52:457] mockFetch enter :: url = taobao.com, ms = 600
[22:43:52:457] mockFetch new promise enter :: url = taobao.com, ms = 600
[22:43:52:457] testAsyncGenWait :: rst1, value = Result mockRst1===>>> mockFetch end :: url = google.com, ms = 800, done = false
[22:43:53:061] mockFetch enter :: url = bd.com, ms = 100
[22:43:53:061] mockFetch new promise enter :: url = bd.com, ms = 100
[22:43:53:062] testAsyncGenWait :: rst2, value = Result mockRst2===>>> mockFetch end :: url = taobao.com, ms = 600, done = false
[22:43:53:166] testAsyncGenWait :: rst3, value = Result mockRst3===>>> mockFetch end :: url = bd.com, ms = 100, done = false
[22:43:53:167] testAsyncGenWait :: rst4, value = asyncGenWithWait end., done = true

這個執行順序其實跟第一個的基本一樣,直接上個時序圖清晰的展示一下:

(JS解釋引擎這裏線咋看着有點不得勁。。。)

三、直接用作異步迭代器

接着第二步的繼續,異步迭代器,就如其名字一樣,可以當迭代器使用,串行執行。

//testAsyncGenWait();
async function testIteratorAsyncGenWait(){
    log(`testIteratorAsyncGenWait :: enter.`);
    let ag = asyncGenWithWait(); //
    for await(let rst of ag){
        log(`testIteratorAsyncGenWait :: in for await, rst = ${rst}`);
    }
}
testIteratorAsyncGenWait();

輸出:

[23:15:59:141] testIteratorAsyncGenWait :: enter.
[23:15:59:162] asyncGenWithWait :: enter.
[23:15:59:162] mockFetch enter :: url = google.com, ms = 800
[23:15:59:162] mockFetch new promise enter :: url = google.com, ms = 800
[23:15:59:969] testIteratorAsyncGenWait :: in for await, rst = Result mockRst1===>>> mockFetch end :: url = google.com, ms = 800
[23:15:59:969] mockFetch enter :: url = taobao.com, ms = 600
[23:15:59:969] mockFetch new promise enter :: url = taobao.com, ms = 600
[23:16:00:572] testIteratorAsyncGenWait :: in for await, rst = Result mockRst2===>>> mockFetch end :: url = taobao.com, ms = 600
[23:16:00:573] mockFetch enter :: url = bd.com, ms = 100
[23:16:00:573] mockFetch new promise enter :: url = bd.com, ms = 100
[23:16:00:675] testIteratorAsyncGenWait :: in for await, rst = Result mockRst3===>>> mockFetch end :: url = bd.com, ms = 100

 

四、如果串行執行異步效率低,如何並行執行。

在有的場景下,各異步的調用間不存在先後關係,但是都需要執行完了。此時就不要用async generator函數,可以直接用generator函數管理,或者直接使用都行。

 

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