天天看點

了解 Generator 的執行

Generator & yield

開局官宣:

sec-generatoryield

,這是對yield的介紹。

同樣巴拉巴拉列了9條,将以上連結中的說明簡化成3條:

1. 在GeneratorFunction内,當遇到yield關鍵字的時候,先将執行上下文設定為yield之後的表達式進行執行,并且将該表達式傳回值作為目前疊代的結果;

2. 将gen上下文從上下文堆棧中移除,将上級(gen之外)上下文(依次)恢複為目前執行的上下文,此過程中需設定gen上下文的評估狀态,以便在上下文恢複時(下一次調用.next)繼續操作疊代;

3. 通過.next方法依次執行疊代器。

先對上面3點有點印象,再來看看 Generator。

Generator 對象是通過 GeneratorFunction 執行傳回的對象,具有可疊代的特性(疊代器協定定義了一種标準的方式來産生一個有限或無限序列的值),關于疊代器詳見

"疊代器"

GeneratorFunction => Generator

GeneratorFunction.prototype
    next 傳回疊代結果
    return 傳入參數作為疊代結果的value并傳回該疊代項,并且結束Generator對象的疊代
    throw 抛出錯誤值,并且結束Generator對象的疊代      

每個疊代結果都包含 done 和 value :

  1. done 表示生成器是否被完成;

  2. value 表示目前的值。

來個例子:

function* iterator1(){
    console.log(1);
    yield '1';
    console.log(2)
    yield *iterator2();
    yield '2';
    console.log(3);
}

function* iterator2(){
    yield '3';
    console.log(4);
    yield '4';
}

function fn1(){
    console.log(5)
    fn2();
    console.log(6)
}

function fn2(){
    console.log(7)
    var iter = iterator1();
    console.log(iter.next());
    console.log(iter.next());
    console.log(iter.next());
    console.log(8);
    console.log(iter.next());
    console.log(iter.next());
    console.log(9);
}
fn1();
/*
* 輸出順序
* var iter = iterator1();    // before : 5 7
* console.log(iter.next());  // 1    {value:1,done:false}
* console.log(iter.next());  // 2    {value:3,done:false}
* console.log(iter.next());  // 4    {value:4,done:false}
* console.log(8);            // 8
* console.log(iter.next());  //      {value:2,done:false}   
* console.log(iter.next());  // 3    {value:undefined,done:true}
* console.log(9);            // 9 after : 6
*/      

看輸出順序(多個Generator嵌套可看作為在外部Generator的某個索引位置插入内部Generator的元素作為疊代項):

  1. fn1被執行,首先輸出 5;

  2. 進入fn2,輸出 7;

  3. fn2中生成iter,并首次調用iter.next(),執行了iterator1裡面第一個yield之前的console,輸出 1,然後輸出 {value: "1", done: false};

  4. 調用第二個iter.next(),進入iterator2中,輸出 2,然後輸出 {value:'3',done:false};

  5. 調用第三個iter.next(),還是進入iterator2,輸出 4,然後輸出 {value:'4',done:false};

  6. 調用fn2中的console.log(8),輸出 8;

  7. 調用第四個iter.next(),這時候iterator2裡面執行完了,繼續執行iterator1的後續代碼,輸出 {value:2,done:false};

  8. 調用第五個iter.next(),繼續iterator1的後續代碼,輸出 3,這時候iterator1的疊代結束,輸出 {value:undefined,done:true};

  9. 調用fn2中的console.log(9),輸出 9;

  10. 調用fn1中的console.log(6),輸出 6。

Generator的任務執行器

Generator通過.next方法來依次做疊代的執行,然而每次都需要手動寫方法調用是個問題。然後便有了疊代任務的執行器,在執行器内将主動調用.next以執行疊代。

如下面例子:

function run(gen){
    const task = gen();
    // 定義一個對象用于存每個疊代結果,傳入result.value 指派到指定對象上
    let result = task.next();

    // 如果疊代未結束,則繼續執行next(),擷取下個疊代結果,以此類推...
    function step(){
        if(!result.done){
            result = task.next(result.value);
            step();
        }
    }
    step();
}

run(function*(){
    let i = 0;
    while(i<10) { 
        yield ++i,
        console.log(i);
    } 
}); 
// 1 2 3 4 5 6 7 8 9 10       

在run(function*(/* ... */))中,先執行GeneratorFunction疊代對象傳回Generator,然後用一個變量來存每次疊代結果...執行過程如下:

  1. result={value:1,done:false},列印 1;

  2. 在step内,result={value:2,done:false},列印 2;

  3. 在step内,result={value:3,done:false},列印 3;

  ...

  10. 在step内,result={value:10,done:false},列印 10;

  11. 在step内,result={value:undefined,done:true},疊代對象被完成。

如果yield後跟的是異步表達式呢?

代碼如下:

// 基于上面的run函數
run(function*(){ 
    const value1=yield fn1();
    console.log('v1',value1);
    const value2 = yield fn2();
    console.log('v2',value2)
})
function fn1(){
    const promise = new Promise(resolve => setTimeout(()=> resolve(' success'),3000));
    promise.then(res=> console.log(res) )
    return promise;
};
function fn2(){
    console.log('fn2');
    return 'fn2';
}
// v1 Promise
// fn2
// v2 fn2
// 3s 後 success      

假如需求需要fn2的執行依賴fn1的異步傳回值,簡單改造一下run執行器試試:

// 修改上面的run函數
function run(gen){
    const iter = gen();
    // result用來存儲每一次疊代結果
    let result = iter.next();

    step();

    function step(){
        // 如果疊代對象未完成
        if(!result.done){
            // 如果是Promise,則在.then之後執行next
            if(result.value instanceof Promise){
                result.value.then(res =>{
                    result = iter.next(res);
                    step();
                })
            }else{
                result = iter.next(result.value);
                step();
            }
        }
    }
}      

以上是沒看co代碼之前針對問題"如果Generator對象疊代過程中某個疊代處理依賴上一個疊代結果該怎麼辦"想到的方法... 在實作方式上是差了些,但也可以用...

co實作的更好看且适用,每次疊代(function, promise, generator, array, object)都包裝成Promise處理,針對不同場景/類型則關注toPromise層的判斷。

對比一下ES7 async/await通過tsc --target es5 後的代碼。

  1. 首先是個__awaiter方法,裡面是 new Promise;

  2. 然後是個__generator方法,裡面是GeneratorFunction。

也是用Promise包裝Generator的模式實作... 把__awaiter摘出來後的代碼:

var run = function (thisArg,_arguments,generator) {
    return new Promise(function (resolve, reject) {
        generator = generator.apply(thisArg, _arguments || [])

        function fulfilled(value) {
            try {
                step(generator.next(value));
            } catch (e) {
                reject(e);
            }
        }

        function rejected(value) {
            try {
                step(generator["throw"](value));
            } catch (e) {
                reject(e);
            }
        }

        step(generator.next());

        function step(result) {
            result.done ? resolve(result.value) : new Promise(function (resolve) {
                resolve(result.value);
            }).then(fulfilled, rejected);
        }
    });
};      

可能有些關聯的文章:

了解 async/await 的執行 分步了解 Promise 的實作 Generator關系圖 co

文章僅供參考!!!關于更多Generator知識,以閱讀文章開頭官方文檔為準,如更多的術語以及它們各代表什麼過程...

學習過程中,多寫幾次總是會記得深刻些。

繼續閱讀