ES6 非同步程式設計之三:Generator續

碼農彭盛發表於2017-02-12

前言

《ES6 非同步程式設計之一:Generator》中實現了一個非同步函式呼叫鏈,它是一個順序呼叫鏈,很類似責任鏈模式,但現實往往不是平鋪直敘的,更多的其實是峰迴路轉,本文將繼續討論更多Generator的用法。

作為函式的Generator

在之前的示例中,我們更多的是把生成器作為一個迭代器來迴圈呼叫每個函式,但忽視了生成器也是個函式,意味著生成器內部可以實現複雜的業務邏輯,以下的程式碼通過yield等待檔案讀取結果並對結果進行特定的處理,


    function* generator() {
        let r1 = yield get(`a`);
        if (r1) {
            let r2 = yield get(`b`);
            if (r2) {
                console.log(yield get(`d`));
            }
        } else {
            console.log(yield get(`c`));
        }
    }
    
    let g = generator();
    g.next();

如果get是個非同步呼叫,以上的程式碼想要能夠執行則需要get函式執行得到結果後呼叫生成器的next方法來推進,這要求get函式能持有生成器物件,這顯然並不容易。

偏函式

偏函式(Partial Function)是對函式定義域的子集定義的函式,形式上就是指定任意部分引數生成一個新的函式,如下:

    function sum(a, b, c) {
        return a + b + c;
    }
    
    function sum1(a) {
        return function(b, c) {
            return a + b + c;
        };
    }
    
    function sum2(a, b) {
        return function(c) {
            return a + b + c;
        };
    }
    
    sum(1, 2, 3) == sum1(1)(2, 3); //true
    sum(1, 2, 3) == sum2(1, 2)(3); //true

一般在設計非同步呼叫api時,我們總是宣告一個引數來接收回撥函式,當和偏函式相結合就變成這樣:

    function get(f, callback) {
        delay(100, function(s) {
            callback(s + `:get ` + f);
        });
    }
    
    get(`a`, func);  //呼叫get時必須立即傳入一個函式
    
    //轉換成偏函式形式:
    function partialGet(f) {
        return function(callback) {
            delay(100, function(s) {
                callback(s + `:get ` + f);
            });
        };
    }
    
    let pGet = partialGet(`a`);   //可以先生成一個函式
    pGet(func);  //需要時再傳入回撥函式執行

從上面的例子中可以發現,偏函式能使定義和執行分離,說來巧了,生成器可用於定義業務邏輯而生成器的next用於推進業務執行,二者也是相互分離的。

生成器和偏函式

基於前面這麼多鋪墊,假設get就是一個偏函式,如下:

    function get(f) {
        return function(callback) {
            delay(100, function(s) {
                callback(s + `:get ` + f);
            });
        };
    }

這意味著,yield get(`a`)使得next函式執行的結果其value屬性值是個函式,該函式的引數是一個能接收get非同步結果的回撥函式,即:

    g.next().value(function(value) {
        g.next(value); //value成為yield的返回並繼續推進業務邏輯
    });

通過遞迴可以不斷的執行生成器的next方法,一個全新的通過生成器來實現業務邏輯的run方法便呼之欲出了,


function run(gen) {
    let g = gen();

    function next(lastValue) {
        let result = g.next(lastValue); //將上一個非同步執行結果傳出給當前的yield並執行下一個yield
        if (result.done) {
            return result.value;
        }
        //value是偏函式返回的新函式,它的引數是個用來接收非同步結果的回撥函式
        result.value(next);  //next作為接收非同步結果的回撥函式
    }

    next();
}

run(generator);

綜合以上例子不難發現另外一個好處,通過偏函式可以使非同步呼叫api不受生成器的侵入,《ES6 非同步程式設計之一:Generator》中實現的非同步呼叫需要將生成器作為引數,有興趣的話你可以嘗試改造一下之前的示例。

現在通過新編寫的run函式就可以來執行本文一開始編寫的那個生成器了。

thunkify

偏函式的方案對api和生成器也是有侵入的,他要求:

  1. api必須是偏函式形式;

  2. 生成器定義業務邏輯,每個yield 後面的函式必須是呼叫偏函式;

第二個問題是本方案的核心機制所要求,但第一個問題我們可以通過thunkify來解決,api依舊按照get(value, callback)方式定義,但定義生成器時需要將非同步api呼叫通過thunkify轉換為偏函式,如下:

    let Thunkify = require(`thunkify`);
    
    let thunkifiedGet = Thunkify(get);
    
    function get(f, callback) {
        delay(100, function(s) {
            callback(s + `:get ` + f);
        });
    
    }
    
    function* generator() {
        let r1 = yield thunkifiedGet(`a`);
        if (r1) {
            let r2 = yield thunkifiedGet(`b`);
            if (r2) {
                console.log(yield thunkifiedGet(`d`));
            }
        } else {
            console.log(yield thunkifiedGet(`c`));
        }
    }

相關文章