JavaScript中的Generator函數

1. 簡介html

Generator函數時ES6提供的一種異步編程解決方案。Generator語法行爲和普通函數徹底不一樣,咱們能夠把Generator理解爲一個包含了多個內部狀態的狀態機node

執行Generator函數回返回一個遍歷器對象,也就是說Generator函數除了提供狀態機,還能夠生成遍歷器對象。Generator能夠此返回多個遍歷器對象,經過這個對象能夠訪問到Generator函數內部的多個狀態。git

形式上Generator函數和普通的函數有兩點不一樣,一是function關鍵字後面函數名前面有一個星花符號「*」,二是,函數體內部使用yield定義(生產)不一樣的內部狀態程序員

執行Generator函數返回的是一個遍歷器對象,這個對象上有一個next方法,執行next方法會返回一個對象,這個對象上有兩個屬性,一個是value,是yield關鍵字後面的表達式的值,一個是done布爾類型true表示沒有遇到return語句,能夠繼續往下執行,false表示遇到return語句。來看下面的語句:github

    function* helloWorldGenerator () {
        yield 'hello';
        yield 'world';
        return 'ending';
    }

    var hw = helloWorldGenerator();
    console.log(hw.next()); //第一次調用,Generator函數開始執行,直到遇到yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield語句後面表達式的值hello,done屬性爲false,表示遍歷尚未結束
    console.log(hw.next()); //第二次調用,Generator函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield語句後面表達式的值world,done屬性值爲false,表示遍歷尚未結束。
    console.log(hw.next()); //第三次調用,Generator函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,則value屬性爲undefined),done屬性爲true,表示遍歷已經執行結束。
    console.log(hw.next()); //第四次調用,此時Generator函數已經執行完畢,next方法返回對戲那個的value屬性爲undefined,done屬性爲true,表示遍歷結束。
    console.log(hw.next()); //第五次執行和第四次執行的結果是同樣的。 

執行結果以下圖:算法

1. 定義Generator函數helloWorldGenerator函數
2. 函數內部有2個yield表達式和一個return語句,return語句結束執行
3. Generator函數的調用方法和普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是調用以後,函數不是當即執行,返回的也不是return語句的結果undefined,而是一個指向內部狀態的指針對象,也就是上面說的遍歷器對象(Iterator Object)
4. 調用遍歷器對象的next方法,狀態指針移動到下一個狀態,返回{value: "hello", done: false}
5. 調用遍歷器對象的next方法,狀態指針移動到下一個狀態,返回{value: "world", done: false}
6. 調用遍歷器對象的next方法,狀態指針移動到下一個狀態,返回{value: "ending", done: true},done爲true,說明已經遇到了return語句,後面已經沒有狀態能夠返回了
7. 調用遍歷器對象的next方法,指針再也不移動,返回{value: undefined, done: true}
8. 調用遍歷器對象的next方法,指針再也不移動,返回{value: undefined, done: true}shell

注意yield表達式後面的表達式,只有當調用next方法,內部指針指向該語句時纔會執行,至關於JavaScript提供了手動的「惰性求值」語法功能。npm

function* gen() {
  yield  123 + 456;
} 

上面代碼中,yield後面表達式123 + 456,不會當即求值,只會在next方法將指針移動到這一句時,纔會求值。編程

yield表達式語句和return語句有類似之處,也有卻別。類似的地方是都能返回緊跟在語句後面的那個表達式的值。卻別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,return語句沒有位置記憶功能。一個函數裏面,只能執行一次return語句,可是能夠屢次執行yield表達式。也就是說Generator能夠逐次生成多個返回值,這也是它的名字的來歷。 json

Generator函數中能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數。看下面代碼:

    function* f () {
        console.log('執行了!')
    }
    var generator = f();
    setTimeout(function () {
        console.log(generator.next()); // 執行Generator函數,只到遇到yield表達式,這裏沒有就直接輸出:"執行了!",函數返回{"done":true}沒有value
    }, 2000);

輸出結果以下:

Generator函數f()中沒有yield表達式,可是仍然仍是一個Generator函數。若是函數f是一個普通函數,在執行var generator = f();的時候就會輸出「執行了!」。可是f()是一個Generator函數,就變成了只有調用next方法的時候,函數f纔會執行。

另外須要注意,yield表達式只能用在Generator函數裏面,用在其餘地方都會報錯。看下面的代碼:

    var arr = [1, [[2, 3], 4], [5, 6]];
    var flat = function* (a) {
        a.forEach(function (item) {
            if (typeof item !== 'number') {
                yield * flat(item)
            } else {
                yield item
            }
        })
    }
    for (let f of flat) {
        console.log(f);
    } 

上面代碼會報錯,由於forEach方法的參數是一個普通函數,可是在裏面使用了yield表達式。能夠把forEach改爲for循環 

    var arr = [1, [[2, 3], 4], [5, 6]];
    var flat = function* (a) {
        var length = a.length;
        for (var i = 0; i < length; i++) {
            var item = a[i];
            if (typeof item !== 'number') {
                yield *flat(item)
            } else {
                yield item;
            }
        }
    }
    for (var f of flat(arr)) {
        console.log(f);
    }

 輸出結果以下:

另外,若是yield表達式用在另一個表達式之中,必須放在圓括號內部。以下:

     function *demo() {
       console.log('hello ' + (yield));
       console.log('world ' + (yield  123));
     }
     var gen = demo();
     console.log(gen.next());
     console.log(gen.next());
     console.log(gen.next());

輸出結果以下:

1.  定義Generator函數demo
2.  函數內部有輸出"hello"+(yield)和「world」+(yield 123)
3.  調用demo方法獲得遍歷器對象gen
4.  調用遍歷器對象的next方法並輸出,注意先執行表達式語句「hello」 + (yield),獲得{value: undefined, done: false},再輸出:「hello undefined」。注意直接輸出yield表達式獲得的結果是undefined,必須使用遍歷器對象的next方法才能獲取yield表達式後面的值
5.  調用遍歷器對象的next方法並輸出,注意先執行表達式語句「worold」 + (yield),獲得{value: 123, done: false},再輸出:「world undefined」。注意直接輸出yield表達式獲得的結果是undefined,必須使用遍歷器對象的next方法才能獲取yield表達式後面的值
6.  調用遍歷器對象的next方法,由於後面已經沒有yield表達式,雖然沒有return語句,判斷依據是否有更多的yield語句爲標準,仍是輸出{value: undefined, done: true}。done的值是true。後面不管調用next方法多少次,都是這個結果。

yield表達式用做函數或者放在賦值表達式的右邊,能夠不加括號。以下:

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

上面說到yield表達式自己輸出的是undefined,也就是說yield表達式自己沒有返回值,或者說老是返回undefined。next方法能夠帶一個參數,該參數會被當作上一個yield表達式的返回值。

     function *f() {
       for(var i=0; true; i++){
        var reset = yield i;
        if(reset) { i = -1 }
       }
     }
     var g = f();
     console.log(g.next());
     console.log(g.next());
     console.log(g.next(true));

 返回結果以下:

上面代碼返回一個能夠無限運行的Generator函數f,若是next方法沒有參數,每次運行到yield表達式,變量reset的值老是yield表達式的值undefined。當next方法帶一個參數true時,變量reset就被重置爲這個參數的值,即true,所以i的值會等於-1,下一輪循環就會從-1開始遞增。這個功能有很重要的語法意義。Generator函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。經過next方法的參數,就有辦法在Generator函數開始運行以後,繼續向函數體內部注入值。也就是說,在Generator函數運行的不一樣階段,從外部向內部注入不一樣的值,能夠調整函數行爲。

看下面的例子:

    function* foo (x) {
        var y = 2 * (yield (x + 1));
        var z = yield (y / 3);
        return (x + y + z);
    }
    var a = foo(5);
    console.log(a.next());
    console.log(a.next());
    console.log(a.next());
    var b = foo(5);
    console.log(b.next());
    console.log(b.next(12));
    console.log(b.next(13));

運行結果以下圖:

1. 申明一個Generator函數foo
2. 調用函數foo,傳入參數5,獲得遍歷器對象a
3. 調用遍歷器對象a的next方法,返回yield關鍵字後面表達式(x + 1)的值,獲得6,返回結果{ value: 6, done: false }。
4. 調用遍歷器對象a的next方法,往下執行,由於執行next的時候沒帶參數,上一次yield表達式的值從6變成undefined,而不是6,y的值是2 * undefined,即爲NaN。本次yield表達式的值是undefined / 3 爲NaN。最後返回結果{ value: undefined, done: false }
5. 調用遍歷器對象a的next方法,往下執行,由於執行next的時候沒有帶參數,上一次yield表達式的值爲從NaN變成undefined,所以z的值是undefined,返回的值爲5 + NaN + undefined,即爲NaN
6. 調用函數foo,傳入參數5,獲得遍歷器對象b
7. 調用遍歷器對象的next方法,返回yield關鍵字後面表達式(x + 1)的值,獲得6,返回結果{ value: 6, done: false }
8. 調用遍歷器對象的next方法,傳參12,所以上一次yield關鍵字後面的表達式的(x + 1)的值從6變爲12,y的值是2 * 12,即爲24。yield關鍵字後面表達式的值爲 (24 / 3),即爲8。最後返回結果{ value: 8, done: false }
9. 調用遍歷器對象的next方法,傳入參數13,所以上一次yield關鍵字後面的表達式(y / 3)的值從8變成13,z的值是13。表達式(x + y + z)的值是(5 + 24 + 13),即42。最後返回結果{ value: 24, done: true }

注意,因爲next方法的參數表示上一個yield表達式的返回值,因此在第一調用next方法時,傳遞參數是無效的。JavaScript引擎直接忽略第一次使用next方法時的參數,只有從第二次使用next方法開始,參數纔是有效的。從語義上說,第一個next方法用來啓動遍歷器對象,因此不用帶參數。

再看一個例子:

    function * dataConsumer () {
        console.log('started')
        console.log(`1.${yield }`)
        console.log(`2.${yield }`)
        return 'result'
    }
    let genObj = dataConsumer()
    genObj.next()
    genObj.next('a')
    genObj.next('b')

 輸出結果:

1. 定義Generator函數dataConsumer
2. 調用dataConsumer函數,獲得遍歷器對象genObj
3. 調用遍歷器對象genObj的next方法,執行執行dataConsumer函數,只到遇到yield表達式爲止。注意第一句輸出「started」,第二句裏就有yield表達式,所以在這裏中止。最終結果是「started」
4. 調用遍歷器對象genObj的next方法,傳入參數‘a’,繼續往下執行,上一次yield表達式的值從undefined變成‘a’,最後輸出1.a
5. 調用遍歷器對象genObj的next方法,傳入參數‘b’,繼續往下執行,上一次yield表達式的值從undefined變成‘b’,最後輸出2.b

上面代碼是一個很直觀的例子,每次經過next方法向Generator函數注入值,而後打印出來。

若是想要第一次調用next方法時就可以輸入值,能夠在Generator函數外面再包一層。

    function wrapper (generatorFunction) {
        return function (...args) {
            let generatorObject = generatorFunction(...args)
            generatorObject.next()
            return generatorObject
        }
    }
    const wrapped = wrapper(function *() {
        console.log(`first input: ${yield }`)
        return 'DONE'
    })
    wrapped().next('hello')

 輸出結果:

上面代碼中,Generator函數若是不用wrapper先包一層,是沒法在第一次調用next方法的時候就輸入參數的。 這個其實在包裝函數wrapper裏面已經先執行了一次next方法了。

2. Generator和Iterator接口的關係

任意一對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。

因爲Generator函數就是遍歷器生成函數,所以能夠把Generator賦值給對象的Symbol.iterator屬性,從而使這個對象具備Iterator接口

     var myIterable = {};
     myIterable[Symbol.iterator] = function *() {
       yield 1;
       yield 2;
       yield 3;
     };
     console.log([...myIterable]);

輸出結果以下:

上面代碼中Generator函數賦值給Symbol.iterator屬性,從而使myIterator對象具備了iterator接口,這樣就能夠被...運算符遍歷了。

Generator函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。

     function *gen() {
     }
     var g = gen();
     console.log(g[Symbol.iterator]() === g); // 輸出true

 上面代碼中,gen是一個Generator函數,調用它會生成一個遍歷器對象g,它的Symbol.iterator屬性也是一個遍歷器對象生成函數,執行後返回它本身。 

for...of循環能夠自動遍歷Generator函數生成的Generator對象,而且不須要調用next方法,看下面的代碼:

    function* foo () {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
        yield 5;
        return 6;
    }
    for (let v of foo()) {
        console.log(v);
    }

 輸出結果以下:

上面代碼中使用for...of循環,以此顯示5個yield表達式的值。這裏須要注意,一旦next方法的返回對象的done屬性爲true,for...of循環就會終止,且不包含該返回對象,因此上面代碼中return語句返回值6,不包括在for...of循環中。

下面是一個利用Generator函數和for...of循環,實現斐波那契數列的例子

    function* fibonacci () {
        let [prev, curr] = [0, 1];
        for (; ;) {
            [prev, curr] = [curr, prev + curr];
            yield curr;
        }
    }
    for (let n of fibonacci()) {
        if (n > 1000) break;
        console.log(n);
    }

輸出結果以下:

從上面代碼能夠看出,使用for...of語句時,再也不須要使用next方法。注意這裏用for...of循環代替了next方法,照樣能夠執行Generator函數。

利用for...of循環,能夠寫出遍歷任意對象(Object)的方法。原生的JavaScript兌現更沒有遍歷接口,沒法使用for...of循環,經過Generator函數爲它加上這個接口,就可使用了。

    function* objectEntries (obj) {
        let propKeys = Reflect.ownKeys(obj);

        for (let propKey of propKeys) {
            yield [propKey, obj[propKey]];
        }
    }

    let jane = {first: 'Jane', last: 'Doe'};
    for (let [key, value] of objectEntries(jane)) {
        console.log(`${key}: ${value}`);
    }

 輸出結果以下:

上面代碼中,原生對象jane不具有Iterator接口,沒法用for...of遍歷。這是咱們經過Generator函數objectEntries爲它加上遍歷器接口,就能夠用for...of遍歷了。加上遍歷器接口的另外一種寫法是,將Generator函數加到對象的Symbol.iterator屬性上,代碼以下:

    function* objectEntries () {
        let propKeys = Object.keys(this);
        for (let propKey of propKeys) {
            yield [propKey, this[propKey]];
        }
    }

    let jane = {first: 'Jane', last: 'Doe'};
    jane[Symbol.iterator] = objectEntries;
    for (let [key, value] of jane) {
        console.log(`${key}: ${value}`);
    }

 輸出結果以下:

除了for...of循環擴展運算符(...)解構賦值Array.from方法內部調用的都是遍歷器接口,這就是說,它們均可以將Generator函數返回的Iterator對象做爲參數。看下面的代碼:

    function* numbers () {
        yield 1
        yield 2
        return 3
        yield 4
    }
    // 擴展運算符
    console.log(...numbers())
    // Array.from方法
    console.log(Array.from(numbers()))
    // 解構賦值
    let [x, y] = numbers()
    console.log(x, y)
    // for ... of循環
    for (let n of numbers()) {
        console.log(n)
    }

輸出結果以下:

3. Generator.property上的方法

3.1. Generator.property.throw()

Generator原型對象上有一個throw方法,能夠在函數體外拋出錯誤,而後在Generator函數體內捕獲。

    var g = function* () {
        try {
            yield;
        } catch (e) {
            console.log('內部捕獲', e)
        }
    }
    var i = g();
    i.next();
    try {
        i.throw('a');
        i.throw('b');
    } catch (e) {
        console.log('外部捕獲', e);
    }

 輸出結果以下:

上面代碼中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被Generator函數體內部的catch語句捕獲。i第二次拋出錯誤,因爲Generator函數內部的catch語句已經執行過了,不會再捕捉到這個錯誤,因此這個錯誤就被拋出了Generator函數體,被函數體外的catch語句捕獲。

throw方法能夠接受一個參數,參數會被catch語句接收,建議拋出Error對象實例。

    var g = function* () {
        try {
            yield;
        } catch (e) {
            console.log(e)
        }
    }
    var i = g();
    i.next();
    i.throw(new Error('出錯了!'))

 輸出結果以下:

不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後者只能被函數體外的catch語句捕獲。

    var g = function* () {
        while (true) {
            try {
                yield;
            } catch (e) {
                if (e !== 'a') {
                    throw e;
                }
                console.log('內部捕獲', e)
            }
        }
    }
    var i = g();
    i.next();
    try {
        throw new Error('a');
        throw new Error('b');
    } catch (e) {
        console.log('外部捕獲', e);
    }

 輸出結果以下:

上面代碼只捕獲了a,是由於函數體外的catch語句塊,捕獲了拋出的a錯誤之後,就不會再繼續try代碼塊裏剩餘的語句了。由於沒有執行i.catch()語句,內部的異常不會被捕獲。

若是Generator函數內部沒有try...catch代碼塊,那麼throw方法拋出的錯誤將被外部try...catch代碼塊捕獲。

    var gen = function* gen () {
        yield console.log('hello');
        yield console.log('world');
    }

    var g = gen();
    g.next();
    g.throw();

 輸出以下:

上面代碼中給,g.throw拋出錯誤後,沒有任何try...catch代碼能夠捕獲這個錯誤,致使程序報錯,終端執行。

throw方法拋出的錯誤要被內部捕獲,前提是必須至少執行一次next方法。

    function * gen () {
        try {
            yield  1
        } catch (e) {
            console.log('內部捕獲')
        }
    }
    var g = gen()
    t.throw(1)

 輸出結果以下:

上面代碼中,g.throw(1)執行時,next方法一次都沒有執行過。這時,拋出的錯誤不會被內部捕獲,而是直接在外部拋出,致使程序出錯。這種行爲其實很好理解,由於第一次執行next方法,等同於啓動執行Generator函數的內部代碼,不然Generator函數尚未開始執行,這時throw方法拋出錯誤只能拋出在函數外部。

throw方法被捕獲之後,會附帶執行下一條yield表達式。也就是說,會附帶執行一次next方法。

    var gen = function* gen () {
        try {
            yield console.log('a');
        } catch (e) {

        }
        yield console.log('b');
        yield console.log('c');
    }
    var g = gen();
    g.next();
    g.throw();
    g.next();

 輸出結果以下:

上面代碼中,g.throw方法被捕獲之後,自動執行了一次next方法。因此會打印b。另外,也能夠看到,只要Generator函數內部部署了try...catch代碼塊,那麼遍歷器的throw方法拋出的錯誤,不影響下一次遍歷。

另外,throw命令和g.throw方法是無關的,二者互不影響。

    var gen = function* gen() {
        yield console.log('hello');
        yield console.log('world');
    }
    var g = gen();
    g.next();

    try {
        throw new Error();
    } catch (e) {
        g.next();
    }

 輸出結果以下:

上面代碼中,throw命令拋出的錯誤不會影響到遍歷器的狀態,因此兩次執行next方法,都進行了正確的操做。

這種函數體內捕獲錯誤的機制,方便了對錯誤的處理。多個yield表達式能夠只用一個try...catch代碼塊來捕獲錯誤。若是使用回調函數的寫法,想要捕獲多個錯誤,就不得不爲每一個函數內部寫一個錯誤處理語句,如今只在Generator函數內部洗寫一次catch語句就能夠了。

Generator函數體外拋出的錯誤,能夠在函數體內捕獲;反過來,Generator函數體內拋出的錯誤,也可被函數體外的catch捕獲。

    function* foo () {
        var x = yield 3;
        var y = x.toUpperCase();
        yield y;
    }

    var it = foo();
    it.next();
    try {
        it.next(42);
    } catch (err) {
        console.log(err);
    } 

上面代碼中,第二個next方法向函數體內傳入一個參數42,,數值是沒有toUpperCase方法的,因此會拋出一個TypeError錯誤,被函數體外的catch捕獲。

一旦Generator執行過程當中拋出錯誤,且沒有被內部捕獲,就不會再往下執行下去了。若是還調用next方法,將返回一個value屬性爲undefined,done屬性爲tru的對象,即JavaScript引擎認爲這個Generator已經運行結束了。

     function *g() {
       yield 1;
       console.log('throwing an exception');
       throw new Error('generator broke!');
       yield 2;
       yield 3;
     }
     function log(generator) {
       var v;
       console.log('starting generator');
       try{
           v = generator.next();
           console.log('第一次運行next方法', v);
       } catch(err) {
           console.log('捕捉錯誤', v);
       }
       try{
           v = generator.next();
           console.log('第二次運行next方法',v);
       }catch(err){
         console.log('捕捉錯誤', v);
       }
       try{
           v = generator.next();
           console.log('第三次運行next方法', v);
       } catch(err) {
           console.log('捕捉錯誤', v);
       }
       console.log('caller done');
     }
     log(g());

 執行結果以下:

上面代碼一共三次運行next方法,第二次運行的時候會拋出錯誤,而後第三次運行的時候,Generator函數就已經結束了,再也不執行下去。

3.2.Generator.property.return()

Generator函數返回的遍歷器對象,還有一個return方法,能夠返回給定值,而且終結遍歷Generator函數。

     function *gen() {
       yield 1;
       yield 2;
       yield 3;
     }
     var g = gen();
     console.log(g.next());
     console.log(g.return('foo'));
     console.log(g.next());

 執行結果以下:

上面代碼中,遍歷器對象g調用return方法以後,返回值的value屬性就是return方法的參數「foo」。而且,Generator函數的遍歷就終止了,返回值的done屬性爲true,之後再調用next方法,done屬性的返回值老是true。

若是return方法調用時,不提供參數,則返回值的value屬性爲undefined。

    function *gen() {
        yield 1;
        yield 2;
        yield 3;
    }
    var g = gen();
    console.log(g.next());
    console.log(g.return());

 執行結果以下:

若是Generator函數內部有try...finally代碼塊,那麼return方法會推遲到finally代碼塊執行完後再執行。

     function * numbers() {
         yield 1;
         try{
             yield 2;
             yield 3;
       } finally {
             yield 4;
             yield 5;
       }
       yield 6;
     }
     var g = numbers();
     console.log(g.next());
     console.log(g.next());
     console.log(g.return(7));
     console.log(g.next());
     console.log(g.next());

 執行結果以下:

上面代碼中,調用return方法後,就開始執行finally代碼塊,而後等到finally代碼塊執行完,再執行return方法。 遇到return語句對象指針就會跳轉到finally裏去執行,只到把finally裏的語句執行完再執行return語句。

next(),throw(),return()方法的共同點

next(),throw(),return()這三個方法本質上是同一件事情,能夠放在一塊兒理解。他們的做用個都是讓Generator函數恢復執行而且使用不一樣的語句替換yield表達式

next()是將yield表達式替換成一個值。

    const g = function* (x, y) {
            let result = yield x + y
            return result
        }
    const gen = g(1, 2)
    console.log(gen.next())
    console.log(gen.next(1))

 輸出結果以下:

上面代碼中,第二個next(1)方法至關於將yield表達式x + y替換成一個值1。若是next方法沒有參數,就至關於替換成undefined。因此第二次調用next方法的時候若是不傳參數,返回的結果是{ value: undefined, done: false }。

throw是將yield表達式替換成一個throw語句。

    const g = function* (x, y) {
            let result = yield x + y
            return result
        }
    const gen = g(1, 2)
    console.log(gen.next())
    gen.throw(new Error('出錯了'))

輸出結果以下:

上面代碼至關於將let result = yield x + y替換成let result = throw(new Error('出錯了'))

return語句時將yield表達式替換成一個return語句

    const g = function* (x, y) {
        let result = yield x + y
        return result
    }
    const gen = g(1, 2)
    console.log(gen.next())
    console.log(gen.return(2))

輸出結果以下:

return語句至關於將let result = yield x + y替換成let result = return 2

4. yield*表達式

若是在Generator函數內部,調用跟另一個Generator函數,默認狀況下是沒有效果的。看下面代碼:

    function* foo () {
        yield 'a';
        yield 'b';
    }

    function* bar () {
        yield 'x';
        foo();
        yield 'y';
    }

    for (let v of bar()) {
        console.log(v);
    }

輸出結果以下 

上面代碼中,foo和bar都是Generator函數,在bar函數中調用foo,是不會有任何效果的。可使用yield*表達式來調用另一個Generator函數。以下代碼:

    function* foo () {
        yield 'a';
        yield 'b';
    }

    function* bar () {
        yield 'x';
        yield *foo();
        yield 'y';
    }

    for (let v of bar()) {
        console.log(v);
    }

執行效果以下:

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}

輸出結果是相同的,在一個Generator函數中使用yield*調用另一個Generator函數,至關於把另外一個Generator函數中的yield表達式放在這個函數中執行。

    function* inner () {
        yield 'hello!';
    }

    function* outter1 () {
        yield 'open';
        yield inner();
        yield 'close';
    }

    var gen = outter1();
    console.log(gen.next().value);
    console.log(gen.next().value);
    console.log(gen.next().value);

    function* outter2 () {
        yield 'open';
        yield* inner();
        yield 'close';
    }

    var gen2 = outter2();
    console.log(gen2.next().value);
    console.log(gen2.next().value);
    console.log(gen2.next().value);

輸出結果以下:

上面代碼中,outer2使用了yield*表達式,outer1沒有使用。結果就是,outer1返回一個遍歷器對象,outer2返回該遍歷器對象的內部值。從語法角度看,若是yield表達式後面跟的是一個遍歷器對象,須要在yield關鍵字後面加上星號,代表它返回的是一個遍歷器對象,這被稱爲yield*表達式。

    let delegatedIterator = (function* () {
        yield 'Hello!';
        yield 'Bye!';
    }());

    let delegatingIterator = (function* () {
        yield 'Greetings!';
        yield* delegatedIterator;
        yield 'Ok, bye.';
    }());

    for (let value of delegatingIterator) {
        console.log(value);
    }

執行結果以下:

上面代碼中,delegatingIterator是代理者,delegatedIterator是被代理者,因爲yield* delegatedIterator語句獲得的值,是一個遍歷器,因此要用星號表示。運行結果是用一個遍歷器遍歷了多個Generator,有遞歸的效果。

yield*後面的Generator函數沒有return語句的時候,等同於在Generator函數內部,部署了一個for...of循環。以下代碼:

     function *concat(iter1, iter2) {
       yield * iter1;
       yield * iter2;
     }
     // 等同於
     function * concat(iter1, iter2) {
       for(var value of iter1){
           yield value;
       }
       for(var value of iter2){
           yield value;
       }
     }

 

上面代碼說明,yield*後面的Generator函數(沒有return語句時),不過是for...of的一種簡寫形式,徹底能夠用後者代替前者。反之,在有return語句的時候須要使用var value = yield* iterator的形式獲取return語句的值。

若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。以下代碼:

    function* gen () {
        yield* ['a', 'b', 'c']
    }
    let g = gen()
    console.log(g.next())
    console.log(g.next())
    console.log(g.next())
    console.log(g.next())

執行結果以下:

上面代碼中,yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。

實際上,任何數據結構,只要有Iterator接口,就能夠被yield*表達式遍歷。

    let read = (function* () {
        yield 'hello';
        yield* 'world';
    })();
    console.log(read.next().value);
    console.log(read.next().value);

返回結果以下:

上面代碼中,yield表達式返回的是整個字符串,可是yield*表達式返回的是單個字符。由於字符串有Iterator接口,因此被yield*表達式遍歷。

若是被代理的Generator函數有return語句,那麼就能夠向代理它的Generator函數返回數據。

    function* foo () {
        yield 2;
        yield 3;
        return "foo";
        yield 4;
    }

    function* bar () {
        yield 1;
        var v = yield* foo();
        console.log("v: " + v);
        yield 5;
    }

    var it = bar();
    console.log(it.next()); // {value: 1, done: false}
    console.log(it.next()); // {value: 2, done: false}
    console.log(it.next()); // {value: 3, done: false}
    console.log(it.next()); // "v: foo" {value: 5, done: true}
    console.log(it.next()); // {value: undefined, done: true} 

執行結果以下:

1. 定義Generator函數foo
2. 定義Generator函數bar,在函數內部使用yield*表達式調用函數foo
3. 調用bar方法,獲得遍歷器對象it
4. 調用遍歷器對象it的next方法,返回{ value: 1, done: false }
5. 調用遍歷器對象it的next方法,返回Generator函數foo的第一個yield表達式返回的對象{ value: 2, done: false }
6. 調用遍歷器對象it的next方法,返回Generator函數foo的第二個yield表達式返回的對象{ value: 3, done: false }
7. 調用遍歷器對象it的next方法,foo結束,foo方法裏面有return語句,返回值是「foo」,繼續往下執行只到遇到yield語句,輸出「v:foo」 並輸出{ value: 5, done: false }
8. 調用遍歷器對象it的next方法,Generator函數裏已經沒有yield語句,輸出{ value: undefiined, done: true }

再看下面的例子

    function* genFuncWithReturn () {
        yield 'a';
        yield 'b';
        return 'The result';
    }

    function* logReturned (genObj) {
        let result = yield* genObj;
        console.log(result);
    }

    console.log([...logReturned(genFuncWithReturn())])

輸出結果:

上面代碼中,存在兩次遍歷,第一是擴展運算符便利函數logReturned返回的遍歷器對象,第二次是yield*語句遍歷函數genFunWithReturn返回的遍歷器對象。這兩次遍歷效果是疊加的,最終表現爲擴展運算符遍歷函數getFuncWithReturn返回的遍歷器對象。這兩次遍歷的效果是疊加的,最終表現爲擴展運算符遍歷函數getFunWithReturn返回的遍歷器對象。因此,最後的數據表達式獲得的值是[ 'a', 'b' ]。可是函數getFuncWithReturn的return語句的返回值「The result」,會返回給函數logReturned內部的result變量,所以會有終端輸出。

yield*命令能夠很方便地取出多維嵌套數組的全部成員。看下面的代碼:

    function* iterTree (tree) {
        if (Array.isArray(tree)) {
            for (let i = 0; i < tree.length; i++) {
                yield* iterTree(tree[i]);
            }
        } else {
            yield  tree;
        }
    }

    const tree = ['a', ['b', 'c'], ['d', 'e']];
    for (let x of iterTree(tree)) {
        console.log(x);
    }

運行結果以下,效果至關於Array.property.flat()

下面的例子稍微複雜,使用yield*語句遍歷徹底二叉樹

    function Tree (left, label, right) {
        this.left = left;
        this.label = label;
        this.right = right;
    }

    // 下面是中序(inorder)遍歷函數,因爲返回的是一個遍歷器,因此要用genrator函數,函數體內採用遞歸算法,因此左樹和右樹都要用yield*遍歷
    function* inorder (t) {
        if (t) {
            yield* inorder(t.left);
            yield t.label;
            yield* inorder(t.right)
        }
    }

    // 下面生成二叉樹
    function make (array) {
        // 判斷是否爲葉子節點
        if (array.length === 1) {
            return new Tree(null, array[0], null);
        }
        return new Tree(make(array[0]), array[1], make(array[2]));
    }

    let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
    var result = [];
    for (let node of inorder(tree)) {
        result.push(node);
    }
    console.log(result); 

輸出結果以下

做爲對象屬性的Generator函數

若是一個對象的屬性是Generator函數,能夠簡寫成下面的形式

    let obj = {
        * myGeneratorMethod () {

        }
    } 

上面代碼中myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個Generator函數。它的完整形式以下:

    let obj = {
            myGeneratorMethod: function* () {

            }
        } 

Generator函數的this 

Generator函數老是返回一個遍歷器,ES6規定這個遍歷器是Generator函數的實例,也繼承了Generator函數的prototype對象上的方法。

    function* g () {
    }

    g.prototype.hello = function () {
        return 'hi!';
    };

    let obj = g();
    console.log(obj instanceof g); // true
    console.log(obj.hello()); // 'hi' 

上面代碼中,Generator函數g返回的遍歷器obj,是g的實例,並且繼承了g.prototype。可是,若是把g當作普通的構造函數,並不會生效,由於g返回老是遍歷器對象,而不是this對象。

    function* g () {
        this.a = 11;
    }

    let obj = g();
    console.log(obj.a);  // undefined 

上面代碼中,Generator函數g在this對象上面添加了一個屬性a,可是obj對象拿不到這個屬性。

Generator函數也不能和new命令一塊兒使用,不然會報錯。

    function* F () {
        yield this.x = 2;
        yield this.y = 3;
    }

    let obj = new F(); // Uncaught TypeError: F is not a constructor 

上面代碼中,new命令和Generator函數F一塊兒使用,結果報錯,由於F不是一個構造函數 。

有沒有辦法讓Generator函數返回一個正常的對象實例,既能夠用next方法,又能夠得到正常的this呢?下面是一個變通方法,首先生成一個空對象,使用call方法綁定Generator函數內部的this。這樣,構造函數調用之後,這個空對象就是Generator函數實例的對象了。

    function* F () {
        this.a = 1;
        yield this.b = 2;
        yield this.c = 3;
    }

    var obj = {};
    var f = F.call(obj);
    console.log(f.next()); // object{value: 2, done: false}
    console.log(f.next()); // object{value: 3, done: false}
    console.log(f.next()); // object{value: undefined, done: true}

    console.log(obj.a); //1
    console.log(obj.b); //2
    console.log(obj.c); //3

執行結果以下:

上面代碼中,首先是F內部的this對象綁定obj對象,而後調用它,返回一個Iterator對象。這個對象執行三次next()方法,(由於F內部有兩個yield表達式),完成F內部全部代碼運行。這是全部內部屬性都綁定在obj對象上路,所以obj對象也就成了F對象的實例。

上面代碼中給,執行的是遍歷器對象f,可是生成的對象實例是obj,有沒有辦法將這兩個對象統一塊兒來呢?一個辦法就是將obj換成F.prototype。

    function* F () {
        this.a = 1;
        yield this.b = 2;
        yield this.c = 3;
    }

    var f = F.call(F.prototype);
    console.log(f.next());
    console.log(f.next());
    console.log(f.next());

    console.log(f.a);
    console.log(f.b);
    console.log(f.c);

執行結果以下:

再將F改形成一個構造函數,就能夠對它執行new命令了,代碼以下:

    function* gen () {
        this.a = 1;
        yield this.b = 2;
        yield this.c = 3;
    }

    function F () {
        return gen.call(gen.prototype);
    }

    var f = new F();
    console.log(f.next());
    console.log(f.next());
    console.log(f.next());

    console.log(f.a);
    console.log(f.b);
    console.log(f.c); 

5 含義

5.1 Generator和狀態機

Generator是實現狀態機的最佳結果。好比,下面代碼中clock函數就是一個狀態機。

    var ticking = true;
    var clock = function () {
        if (ticking) {
            console.log('Tick!')
        } else {
            console.log('Tock!')
        }
        ticking = !ticking;
    }
    clock();
    clock(); 

clock函數有兩種狀態(Tick和Tock) ,每運行一次,就改變一次狀態。這個函數若是用Generator函數實現,就是像下面這樣:

    var clock = function* () {
        while (true) {
            console.log('Tick!');
            yield;
            console.log('Tock!');
            yield;
        }
    };
    var c = clock();
    c.next();
    c.next(); 

和上面不用Generator函數的方法比較,少了用來保存狀態的外部變量ticking,這樣更加簡潔,安全(狀態不會被外面代碼篡改) ,更符合函數式編程的思想,在寫法上也更加優雅。Generator之因此能夠不用外部變量保存,由於它自己就包含了一個狀態信息,即目前是否處於暫停狀態。

5.2 Generator與協程

 協程(coroutine)是一種程序運行的方式,能夠理解爲「協做的線程」或者「協做的函數」。協程能夠用單線程實現,也能夠用多線程實現。

(1)協程與子例程的差別

傳統的「子例程」(subroutine)採用堆棧式的「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停狀態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到收回執行權的時候,再恢復執行。這種能夠並行執行,交換執行權的線程(或函數),就稱爲協程。

從實現上來看,在內存中給,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代碼,實現多任務的並行。

(2)協程與普通線程的差別

協程適用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文,能夠分享全局變量。他們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是同一時間運行的協程只有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。

因爲JavaScript是單線程語言,只能保持一個調用棧。引入協程後,每一個任務能夠保持本身的調用棧。這樣作的最大好處是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。

Generator函數是ES6對協程的實現,可是屬於不徹底實現。Generator函數是「半協程」,意思是隻有Generator函數的調用者,才能將程序的執行權交給Generator函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。

若是將Generator函數當作協程,徹底能夠將多個須要相互協做的任務寫成Generator函數,他們之間使用yield表達式交換控制權。

5.3 Generator與上下文

JavaScript代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),它包含了當前全部變量和對象。而後,執行函數(或者塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此產生一個上下文環境的堆棧(context statck)。

這個堆棧式「先進後出」的數據結構,最後產生的上下文環境首先執行完成,退出堆棧,而後執行完成它下層的上下文,直至全部代碼執行完成,堆棧清空。

Generator函數不是這樣,它執行產生的上下文環境,一旦遇到yied命令,就會暫時退出堆棧,可是並不消失,裏面全部變量和對象會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會從新加入調用棧,凍結的變量和對象恢復執行。

    function * gen () {
        yield 1;
        return 2;
    }
    let g = gen();
    console.log(g.next().value, g.next().value); 

輸出結果:

上面的代碼中,第一次執行g.next()時,Generator函數的gen的上下文會加入堆棧,機開始運行gen內部的代碼。等到遇到yield 1的時候,gen上下文退出堆棧,內部狀態凍結。第二次執行g.next()時,gen上下文又從新加入堆棧,變成當前的上下文,從新恢復執行。

5.4 應用

Generator能夠暫停函數執行,返回yield表達式的值。這種特色使得Generator函數有多種應用場景。

5.4.1 異步操做的同步化表達

Generator函數的暫停執行的效果,意味着能夠把異步操做寫在yield表達式裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了。由於異步操做的後續操做能夠放在yield表達式下面,反正要等到調用next方法時再執行,因此Generator函數的一個重要時機意義就是用來處理異步操做,改寫回調函數。看下面的代碼段:

    function* loadUI () {
        showLoadingScreen();
        yield loadUIDataAsynchronously();
        hideLoadingScreen();
    }

    var loader = loadUI();
    // 加載UI
    loader.next();
    // 卸載UI
    loader.next(); 

上面代碼中,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對改遍歷器調用next方法,則會顯示Loaidng界面(showLoadingScreen),而且異步加載數據(loadingUIDataAsynchronously)。等到數據加載完成,再一次調用next方法,則會隱藏Loading界面。能夠看到,這種寫法的好處是全部Loading界面的邏輯,都會被封裝在一個函數裏,循序漸進很是清晰。

下面是一個例子,能夠手動逐行讀取一個文本文件。

    function* numbers () {
        let file = new FileReader('numbers.txt');
        try {
            while (!file.eof) {
                yield parseInt(file.readLine(), 10);
            }
        } finally {
            file.close();
        }
    } 

5.4.2 流程控制

若是有一個多步操做很是耗時,採用回調函數,可能寫成下面這樣:

    setp1(function (value1) {
        setp2(value1, function (value2) {
            setp3(value2, function (value3) {
                // Dom something with value3
            })
        })
    }) 

採用Promise改寫上面的代碼。代碼中把回調函數,改爲直線執行的形式,可是加入了大量的Promise語法。

    Promise.resolve(setp1)
        .then(setp2)
        .then(setp3)
        .then(setp4)
        .then(function (value4) {
            // Do something with value4
        }, function (error) {
            // Handle any error from stemp1 through step4
        }).done() 

採用Generator語法,能夠寫成下面這樣:

    function* longRunningTask (value1) {
        try {
            var value2 = yield step1(value1);
            var value3 = yield setp2(value2);
            var value4 = yield setp3(value3);
            var value5 = yield setp4(value4);
            // Do something with value4
        } catch (e) {
            // handle error
        }
    }
    // 而後使用一個函數,依次自動執行全部步驟
    scheduler(longRunningTask(initValue));

    function scheduler (task) {
        var taskObj = task.next(task.value);
        if (!taskObj.done) {
            task.value = taskObj.value;
            scheduler(task);
        }
    } 

注意,上面這種作法,只適合同步操做,即全部的task都必須是同步的,不能有異步操做。由於這裏的代碼獲得返回值,當即繼續往下執行,沒有判斷異步操做什麼時候完成。

下面使用for...of循環會一次執行yield命令的特性,提供一種更通常的控制流程管理的方式。

    let step = [step1Func, setp2Func, setp3Fund];
    function* iterateSteps () {
        for (var i = 0; i < step.length; i++) {
            var step = step[i];
            yield step();
        }
    } 

上面代碼中,數組steps封裝了一個任務的多個步驟,Generator函數iterateSteps則是一次爲這些步驟添加上yield命令。

將任務分解成步驟以後,還能夠將項目分解成多個執行的任務。

    let jobs = [job1, job2, job3];

    function* iterateJobs (jobs) {
        for (var i = 0; i < jobs.length; i++) {
            var job = jobs[i];
            yield* iterateSteps(job.setps); // 在Generator函數內部調用另一個Generator函數
        }
    } 

上面代碼中,數組jobs封裝了一個項目的多個任務,Generator函數iterateJobs則以此爲這些任務加上yield*命令。

最後,就可使用for...of循環以此執行全部任務的全部步驟。

    for (var setp of iterateJobs(jobs)) {
        console.log(step.id);
    } 

再次提醒,上面的作法只能用於全部步驟都是同步操做的狀況,不能有異步操做的步驟。

for...of本質是wihie循環,因此上面的代碼實質上執行的是下面的邏輯。

    var it = iterateJobs(jobs);
    var res = it.next();
    while (!res.done) {
        var result = res.value;
        res = it.next();
    } 

5.4.3 部署Iterator接口

利用Generator函數,能夠在任意對象上部署Iterator接口。

    function* iterEntries (obj) {
        let keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            let key = keys[i];
            yield [key, obj[key]];
        }
    }

    let myObj = {foo: 3, bar: 7};
    for (let [key, value] of iterEntries(myObj)) {
        console.log(key, value);
    }
    for (let [key, value] of Object.entries(myObj)) {
        console.log(key, value);
    }
    for (let key in myObj) {
        console.log(key);
    } 

輸出結果以下:

上面代碼中,myObj是一個普通對象,經過iterEntries函數,就有了Iterator接口。就是說能夠在任意對象上部署next方法。此外還可使用Object.keysObject.valuesObject.entriesfor...in來遍歷對象

 上述代碼中,myObj是一個普通對象,經過iterEntries函數,就有了Iterator接口。也就是說,能夠在任意對象上部署next方法。

下面例子是對數組部署Iterator接口的例子,儘管數組原生具備這個接口。

    function * makeSimpleGenerator (array) {
        var nextIndex = 0
        while (nextIndex < array.length) {
            yield array[nextIndex++]
        }
    }
    var gen = makeSimpleGenerator(['yo', 'ya'])
    console.log(gen.next())
    console.log(gen.next())
    console.log(gen.next())
    console.log(gen.next()) 

執行結果以下:

5.4.4 做爲數據結構

 Generator能夠看作是一個數據結果,更確切的說,能夠看作一個數組結構,由於Generator函數能夠返回一系列的值,這意味着它能夠對任意表達式,提供相似數組的接口。

    function* doStuff () {
        yield fs.readFile.bind(null, 'hello.txt');
        yield fs.readFile.bind(null, 'world.txt');
        yield fs.readFile.bind(null, 'and-such.txt');
    }

    for (task of doStuff()) {
        // task是一個函數,能夠像回調函數那樣使用它。

上面代碼就是一次返回三個函數,可是因爲使用了Generator函數,致使能夠像處理數組那樣,處理這三個返回的函數。

實際上,若是用ES5表達,徹底可使用數組模擬Generator的這種用法。

    function doStuff () {
        return [
            fs.readFile.bind(null, 'hello.txt'),
            fs.readFile.bind(null, 'world.txt'),
            fs.readFile.bind(null, 'and-such.txt')
        ]
    } 

上面的函數,能夠用如出一轍的for...of循環處理。比較一下能夠看出Generator是的數組或者操做,具有了相似數組的接口。 

6. 異步編程

6.1 概念

異步

異步編程在JavaScript語言中很重要。JavaScript語言的執行環境是「單線程的」的,若是沒有異步編程,不可思議。

ES6以前,異步編程的方法,大概是4種:回調函數事件監聽訂閱/發佈Promise對象。Generator函數將JavaScript異步編程帶入一個全新的階段。

所謂「異步」,簡單的說就是一個任務不是連續完成的,能夠理解爲任務被認爲地分紅兩段,先執行第一段,而後轉而執行其餘的任務,等作好準備,再回頭執行第二段。

好比,有一個任務是讀取文件進行處理,任務的第一段向是操做系統發出請求,要求讀取文件。而後執行其餘任務,等到操做系統返回文件,再接着執行第二段(處理文件)。這種不連續的執行,就叫異步執行。

相應的,連續的執行就叫同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。

回調函數

JavaScript語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏,等到從新執行這個任務的時候,就直接調用這個函數。回調函數的英文名字是callback,是「從新調用」的意思。

讀取文件進行處理,是這樣的:

s.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
}); 

上面代碼中,readFile函數的第三個參數,就是回調函數,也就是任務的第二階段。等到操做系統返回了/etc/passwd這個文件以後,回調函數纔會執行。

一個有趣的問題是,爲何Node約定,回調函數的參數裏,必需要有一個錯誤對象err(若是沒有錯誤,這個參數是null)?

緣由是執行分紅兩段,第一段執行完成後,任務所在的上下文環境就已經結束了。在這之後拋出的錯誤,原來的上下文環境已經沒法捕捉,只能當作參數,傳入第二段。

Promise

回調函數自己並無問題,它的問題出在多個回調函數嵌套。假設讀取A文件以後,再讀取B文件,代碼以下:

fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
}); 

若是依次讀取兩個以上的文件,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會擠成一團,沒法閱讀。由於多個異步操做構成了強耦合,只要有一個操做須要修改,它的上層回調函數和下層回調函數,可能就要跟着修改,這種狀況被稱爲「回調函數地獄」。

Promise對象就是爲了解決這個問題而被提出的。它不是新的語法功能,而是一種新的寫法,容許將回調函數的嵌套,改爲鏈式調用。採用Promise,連續讀取多個文件的寫法以下:

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
}); 

上面代碼中,使用了fs-readfile-promise模塊,它的做用就是返回一個Promise版本的readFile函數。Promise提供then方法加載回調函數,catch方法捕捉執行過程當中拋出的錯誤。

能夠看到,Promise的寫法只是回調函數的改進,使用then方法之後,異步任務的兩段執行看着更清楚了,除此以外,並沒有新意。

Promise有一個問題是代碼冗餘,原來的任務被Promise包裝了一下,無論怎麼操做,看上去的一堆then方法,原來的語義變得很不清楚了

7. Generator函數處理異步

協程

傳統的編程語言,早有異步編程的解決方案(多任務解決方案)。其中有一種叫「協程(coroutine)」,意思是:多個線程協做完成任務。協程有點像函數,又有點像線程。它的運行流程以下:

第一步:協程A開始執行
第二步:協程A執行到一半,進入暫停狀態,執行權交轉移到協程B
第三步:(一段時間後)協程B交換執行權
第四步:協程B恢復執行

舉例來講,讀取文件的協程寫法以下:

function* asyncJob() {
  // ...其餘代碼
  var f = yield readFile(fileA);
  // ...其餘代碼

上面代碼中asyncJob是一個協程,它的關鍵就在yield表達式。yield命令表示執行到此處,將執行權交給其餘的協程。也就是說,yield命令是異步任務的兩個階段的分界線。協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行。它的最大的優勢,就是代碼的寫法很是像同步操做,若是去除yield命令,就是如出一轍的。

協程的Generator函數實現

Generator函數是協程在ES6的實現,最大特色就是能夠交出函數的執行權(暫停執行)。整個Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操做須要暫停的地方,都用yield表達式註明。Generator函數的執行方法以下:

  function* gen (x) {
      var y = yield x + 2;
      return y;
  }

  var g = gen(1);  // 調用Generator函數,傳入參數1,返回指針
  console.log(g.next()); // 移動指針,直至遇到yield表達式,返回{value: 3, done: false}
  console.log(g.next(2)); // 移動指針,傳入參數2,做爲上一次yield表達式的返回值,賦給y,返回{value: 2, done: true}
  console.log(g.next(2)); // 返回{value: undefined, done: true} 

執行結果以下:

1. 定義Generator函數gen,function後面有星花「*」,內部有yield表達式
2. 調用Generator,傳入參數1,返回一個內部狀態的指針對象g
3. 第一次調用Generator對象g的next方法,Generator函數開始執行,知道遇到第一個yield表達式,返回yield表達式的值value是3,當前狀態done的值false
4. 第二次調用Generator對象g的next方法,Generator函數從上次yield表達式停下的地方往下執行,遇到return語句,返回return表達式的值value是2(傳入參數的值是2,上次yield表達式的值就是2,而不是x + 2 = 3)
5. 第三次調用Generator對象g的next方法,Generator函數上內上次執行的是return語句,不會再往下執行,返回yield表達式的值value是undefined,當前狀態是done不變

上面代碼中,調用Generator函數,會返回一個內部指針(即遍歷器)g。這是Generator函數不一樣於普通函數的,即執行它不會返回內部語句的return語句的結果,而是一個內部狀態的指針對象。調用指針g的next方法,會移動內部指針(即執行異步任務的第一階段),執行內部語句,只到遇到第一個yield語句,上面是x + 2。

next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,便是否還有下一個階段。

Generator函數的數據交換和錯誤處理

Generator函數能夠暫停執行恢復執行,這是它能封裝異步任務的根本緣由。此外,它還有兩個特性是它能夠做爲異步編程的完整解決方案:函數內外的數據交換和錯誤處理機制。

next返回值的value屬性,是Generator函數向外部輸出的數據,next方法還能夠接受參數,向Generator函數體內輸入數據。

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

上面代碼中,第一個next方法的value屬性,返回表達式x + 2的值3。第二個next方法帶有參數2,這個參數能夠傳入Generator函數內部,做爲上顎階段任務的返回結果,被函數體內部的變量y接收。所以,這一步的value屬性,返回的就是2(變量y的值) 。

Generator函數內部還能夠部署錯誤處理代碼,不作函數體外拋出的錯誤。

  function* gen (x) {
      try {
          var y = yield x + 2;
      } catch (e) {
          console.log(e);
      }
      return y;
  }

  var g = gen(1);  // 調用Generator函數,傳入參數1,返回指針
  console.log(g.next()); // 指針移動,直至遇到yield表達式,返回value:3, done:false,  這是由於後面還有一個return
  console.log(g.throw('出錯了!!!!')); // {value: undefined, done: true} 使用指針對象的throw方法拋出錯誤,在函數體內被catch捕獲並傳遞錯誤信息,輸出錯誤信息,返回done:true,沒有value屬性
  console.log(g.next()) // {value: undefined, done: true} 

 執行結果以下:

1. 定義Generator函數,function後面有星花,函數內部有yield表達式
2. 調用Generator函數gen,返回指向內部狀態的指針對象g
3. 第一次調用對象g的next方法,指針移動,只到執行到第一個yield表達式,返回{value: 3, done: false}
4. 調用對象g的throw方法,拋出錯誤,函數內部的try...catch捕獲錯誤,輸出錯誤,返回結果{value: undefined, done: true}。
5. 第二次調用對象g的next方法,輸出{value: undefined, done: true}

上面代碼在Generator函數體外,使用指針對象的throw方法拋出的錯誤,能夠被函數體內的try...catch代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼分離開了,這對異步編程很重要。

異步任務的封裝

看下面的代碼如何使用Generator函數執行一個異步任務

    var fetch = require('node-fetch');
    function* gen () {
        var url = 'https://api.github.com/users/github';
        var result = yield fetch(url);
        console.log(result);
    }

    var g = gen(); // 執行Generator函數,獲取指針
    var result = g.next(); // 移動指針,執行函數,直至遇到yield表達式fetch(url),它執行的是異步操做
    result.value.then(function (data) { // fetch返回的是一個Promise對象,所以要用then方法調用下一個next方法
        console.log(data)
    }).then(function (data) {
        g.next();
    })

上面代碼中,Generator函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從JSON格式的數據分析信息。執行Generator函數以前,先獲取遍歷對象,而後用next方法執行,執行異步任務的第一階段。因爲Fetch模塊返回的是一個Promise兌現給,所以要用then方法調用下一個next方法。

Thunk

Thunk函數是自動執行Generator函數的一種方法。Thunk函數在編程語言剛剛起步的時候被提出,即求值策略,函數的參數到底應該在何時求值問題。

Thunk函數的含義

編譯器的「傳名調用」實現,每每是將參數放到一個臨時函數中,再將這個臨時函數傳入函數體。這個臨時函數就叫作 Thunk函數。

    let x = 1;
    function f(m) { return m * 2; } console.log(f(x + 5)); // 等同於  let x = 1; var thunk = function () { return x + 5; } function f(thunk) { return thunk() * 2; } console.log(f(thunk)) 

輸出結果以下:

 

上面的代碼中,先定義函數f,調用函數的時候傳入表達式x + 5,那這個參數何時替換成6呢?一種方式是「傳值調用」 ,即在進入函數體以前,就計算x + 5的值(等於6),再將值傳入函數f。C語言就是採用這種方式。

另外一種是「傳名調用」,即將表達式x + 5傳入函數體,只在用到它的時候求值。Haskell語言t採用這種策略。

兩種方式哪種更好呢?回答是各有利弊。傳值調用比較簡單,可是對參數求值的時候尚未用到這個參數,可能形成性能損失。

編譯器的「傳名調用」實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作「Thunk函數」。

JavaScript語言中的Thunk函數

JavaScript語言是傳值調用,它的Thunk函數含義全部不一樣。在JavaScript語言中Thunk函數替換的不是表達式,而是多參數函數將其替換成一個只接受回調函數做爲參數的單參數函數。

    // 正常版本的readFile(多參數版本)
    fs.readFile(fileName, callback);

    // Thunk版本的readFile(單參數版本)
    var Thunk = function (fileName) {
        return function (callback) {
            return fs.readFile(fileName, callback)
        }
    }
    var readFileThunk = Thunk(fileName);
    readFileThunk(); 

上面代碼中,fs模塊的readFile方法是一個多參數函數,兩個參數分別爲文件名和回調函數。通過轉換器處理,它變成一個單參數函數,只接受回調函數做爲參數。這個單參數版本,就叫作Thunk函數。

任何函數,只要參數有回調函數,就可以改寫成Thunk函數的形式。下面是個Thunk函數轉換器。

    //ES5版本
    var Thunk = function (fn) {
        return function () {
            var args = Array.prototype.slice.call(arguments); // 使用call方法改變slice函數運行上下文,arguments雖然不是數組,slice(0, end)獲得參數數組
            return function (callback) {
                args.push(callback);
                return fn.apply(this, args);
            }
        }
    }

    //ES6版本
    const Thunk = function (fn) {
        return function (...args) {
            return function (callback) {
                return fn.call(this, ...args, callback);
            }
        }
    } 

ES5版本

1. 定義Thunk函數,傳入參數fn
2. Thunk函數體內返回一個函數對象
3. 在函數內,使用Array.property.slice.call方法獲取外層函數的參數,返回數組,放在args中
4. 在函數內,再返回一個函數,把callback放在參數數組args的末尾
5. 最後在參數上使用apply方法調用,上下文環境爲當前Thunk對象,傳入參數爲數組args

ES6版本
1. 定義Thunk函數,傳入參數fn
2. Thunk函數體內返回一個函數對象,參數使用擴展運算符...將參數args轉爲都好分割的參數序列
3. 在函數體內,再返回一個函數,傳入參數爲callback
4. 最後使用call方法,在當前對象上調用fn方法,傳入參數爲args分隔號的參數序列和callback

使用上面的轉換器,生成fs.readFile的Thunk函數

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

 

下面是一個完整的例子

   function f (a, cb) {
       cb(a);
   }

   const ft = Thunk(f);
   ft(1)(console.log) // 輸出1

1. 定義一個有兩個參數的函數f,第二個參數是回調函數,在函數體內調用回調函數並傳入第一個參數
2. 調用Thunk轉換器,傳入參數f,f被轉換成Thunk函數ft
3. 調用ft,傳入參數1,返回的是內部函數,再傳入參數console.log,最後在1上執行console.log(1),輸出1

看上去是先傳入參數獲得一個函數,而後當即執行這個函數並傳入回調函數做爲參數,只是在調用的時候減小了參數。

Thunkify模塊

在生產環境,可使用Thunkify模塊,使用命令npm install thunkify安裝Thunkify模塊。使用方式以下:

    var thunkify = require('thunkify');
    var fs = require('fs');
    var read = thunkify(fs.readFile);
    read('package.json')(function (err, str) {
        // 回調函數的函數體
    }) 

Thunkif的源碼和上面的轉換很像

    function thunkify (fn) {
        return function () {
            var args = new Array(arguments.length);
            var ctx = this;

            for (var i = 0; i < args.length; ++i) {
                args[i] = arguments[i];
            }

            return function (done) {
                var called;

                args.push(function () {
                    if (called) return;
                    called = true;
                    done.apply(null, arguments);
                });

                try {
                    fn.apply(ctx, args);
                } catch (err) {
                    done(err);
                }
            }
        }
    }; 

在代碼裏多了一個檢查機制,變量called確保回調函數只運行一次。這樣設計與下文的Generator函數相關。

   function f (a, b, callback) {
       var sum = a + b;
       callback(sum);
       callback(sum);
   }

   var ft = thunkify(f);
   var print = console.log.bind(console);
   ft(1, 2)(print); // 輸出3 

上面代碼中,因爲thunkify值容許回到函數執行一次,因此只輸出一行結果。

Generator函數的流程管理

Thunk函數有什麼用呢? 只是爲了減小參數嗎?在ES6中有了Generator函數,Thunk函數能夠用於Generator函數的自動流程管理。Generator函數能夠自動執行。

function* gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
} 

上面代碼中,Generator函數gen會自動執行完全部步驟。

可是這不適合異步操做。若是必須保證前一步執行完,才能執行後一步,上面的自動執行就不可取。這時Thunk函數就能排上用場。以讀取文件爲例,下面的Generator函數封裝了兩個異步操做。

     var fs = require('fs');
     var thunkify = require('thunkify');
     var readFileThunk = thunkify(fs.readFile);

     var gen = function* () {
       var r1 = yield readFileThunk('/etc/fstab');
       console.log(r1.toString());
       var r2 = yield readFileThunk('/etc/shells');
       console.log(r2.toString());
     }

     var g = gen();
     var r1 = g.next();
     r1.value(function (err, data) {
       if (err) throw err;
       var r2 = g.next(data);
       r2.value(function (err, data) {
         if (err) throw err;
         g.next(data);
       })
     })

上面代碼中,使用yield命令將程序的執行權移除Generator函數,那麼就須要一種方法再將執行權交還給Generator函數。手動執行指針對象g是Generator函數的內部指針,表示目前執行到哪一步。next方法負責將指針移動到下一步,並返回當前這一步的信息(即yield表達式的值,包含value屬性和done屬性)。這裏的自動執行步驟實際上是返回調用g.next()方法。下面咱們將探討如何用Thunk函數調用Generator函數自動執行。

     function run(fn) {
       var gen = fn();

       function next(err, data) {
         var result = gen.next(data);
         if (result.done) return;
         result.value(next);
       }
       next();
     }
     function *g () {
         yield 1;
         yield 2;
         return 3;
     }
     run(g) 

上面代碼的run函數,就是一個 Generator 函數的自動執行器。內部的next函數就是 Thunk 的回調函數。next函數先將指針移到 Generator 函數的下一步(gen.next方法),而後判斷 Generator 函數是否結束(result.done屬性),若是沒結束,就將next函數再傳入 Thunk 函數(result.value屬性),不然就直接退出。

有了這個執行器,執行 Generator 函數方便多了。無論內部有多少個異步操做,直接把 Generator 函數傳入run函數便可。固然,前提是每個異步操做,都要是 Thunk 函數,也就是說,跟在yield命令後面的必須是 Thunk 函數。

    var g = function* () {
            var f1 = yield readFileThunk('fileA');
            var f2 = yield readFileThunk('fileB');
            var fn = yield readFileThunk('fileN');
        };
    run(g); 

上面代碼中,函數g封裝了n個異步讀取文件操做,只要執行run函數,這些操做就會自動完成。這樣異步操做不只能夠寫的像同步函數,並且一行代碼就能夠所有執行。

Thunk函數不是Generator函數自動執行的惟一方法。自動執行的關鍵是,必須有一種機制,自動控制Generator函數的流程,接收和交還程序的執行權。回調函數能夠作這一點,Promise也能夠作到。

co模塊

co模塊是著名程序員TJ Holowaychuk於2013年6月發佈的一個小工具,用於Generator函數的自動執行。

下面是一個Generator函數,用於以此讀取兩個文件。

var readFile = require('fs-readfile-promise');
var gen = function *() {
    var f1 = yield readFile('./a.txt');
    var f2 = yield readFile('./b.txt');
    console.log(f1);
    console.log(f2);
}
var co = require('co');
co(gen).then(function () {
    console.log('Generator函數執行完成')
}); 

結果以下

注:這裏是在node.js環境下才能執行,在html頁面中不能執行上面的代碼。

上面代碼中,Generator函數只要傳入co函數中,就會自動執行。co函數返回一個Promise對象,所以能夠用then方法添加回調函數。

co模塊的原理

co爲何能夠自動執行Generator函數呢?前面說過,Generator函數就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,就能自動交會執行權。

有兩種該方法能夠作到這一點
(1) 回調函數,將異步操做包裝成Thunk函數,在回調函數裏交回執行權。
(2) Promise對象,將異步操做包裝成Promise對象,用then方法交回執行權。

co模塊其實就是將兩種自動執行器(Thunk函數和Promise對象),包裝成一個模塊。使用co的前提條件是,Generator函數的yield命令後面,必定是Thunk函數或者Promise對象。若是數組或對象的成員,所有都是Promise對象,也可使用co。

基於Promise對象的自動執行

上面介紹了Thunk函數的自動執行器,下面來看基於Promise對象的自動執行器,這是理解co模塊必須的。繼續使用上面的例子。首先把fs模塊的readFile方法包裝成一個Promise對象。

var fs = require('fs');
var readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function (error, data) {
            if (error) return reject(error);
            resolve(data);
        })
    })
}
var gen = function *() {
    var f1 = yield readFile('./a.txt');
    var f2 = yield readFile('./b.txt');
    console.log(f1.toString())
    console.log(f2.toString())
};
var g = gen();
g.next().value.then(function (data) {
    g.next(data).value.then(function (data) {
        g.next(data)
    })
}) 

上面代碼中手動執行Generator函數,執行結果以下:

手動執行其實就是用then方法,層層添加回調函數。知道這一點就能夠寫一個自動執行器。

function run(gen) {
    var g = gen();
    function next(data) {
        var result = g.next(data);
        if (result.done) return result.value;
        result.value.then(function (data) {
            next(data);
        });
    }
    next();
}
run(gen); 

自動執行器執行結果和上面是同樣的。上面代碼中,只要Generator函數尚未執行到最後一步,next函數就調用自身,以此實現自動執行。

co模塊的源碼

co就是上面的自動執行器的擴展,它的源代碼很少,只有幾十行,很簡單。首先,co函數接受Generator函數做爲參數,返回一個Promise對象。

    function co(gen) {
        var ctx = this;
        return new Promise(function (resolve, reject) {
        });
    } 

在返回的Promise對象裏面,co先檢查參數gen是否爲Generator函數。若是是就執行該函數,獲得一個內部指針對象,若是不是就返回,並將Promise對象的狀態改成resolved。以下:

    function co(gen) {
        var ctx = this;

        return new Promise(function(resolve, reject) {
            if (typeof gen === 'function') gen = gen.call(ctx);
            if (!gen || typeof gen.next !== 'function') return resolve(gen);
        });
    } 

接着,co將Generator函數的內部指針對象的next方法,包裝成onFulfilled函數。這主要是爲了可以捕捉拋出的錯誤。

    function co(gen) {
        var ctx = this;

        return new Promise(function(resolve, reject) {
            if (typeof gen === 'function') gen = gen.call(ctx);
            if (!gen || typeof gen.next !== 'function') return resolve(gen);

            onFulfilled();
            function onFulfilled(res) {
                var ret;
                try {
                    ret = gen.next(res);
                } catch (e) {
                    return reject(e);
                }
                next(ret);
            }
        });
    } 

最後,就是關鍵的next函數,它會反覆調動本身。

    function next(ret) {
        if (ret.done) return resolve(ret.value);
        var value = toPromise.call(ctx, ret.value);
        if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
        return onRejected(
            new TypeError(
                'You may only yield a function, promise, generator, array, or object, '
                + 'but the following object was passed: "'
                + String(ret.value)
                + '"'
            )
        );
    } 

上面的next函數,內部一共只有四行命令:

1. 根據ret的done屬性檢查當前是否爲Generator函數的最後一步,若是是就返回。
2. 確保每一步的返回值,都是Promise對象。
3. 使用then方法,爲返回值加上回調函數,而後經過onFulfilled函數再次調用next函數。
4. 在參數不符合要求的狀況下(參數非Thunk函數和Promise對象),將Promise對象的狀態改成rejected,從而終止執行。

處理併發的異步操做

co支持併發的異步操做,即容許某些操做同時進行,等到他們所有完成,才進行下一步。要把併發的操做都放在數組對象裏面,跟在yield語句後面。

// 數組的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

// 對象的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror); 

下面是另外一個例子

co(function* () {
  var values = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
  // do something async
  return y
} 

上面的代碼,容許併發三個somethingAsync異步操做,等到他們所有完成,纔會進行下一步。

處理Stream

Node提供Stream(流媒體)模式讀寫數據,特色是一次只處理數據的一部分,數據分紅一塊塊以此處理,就像「數據流」同樣。這樣對於處理大規模數據很是有利。Steam模式使用EventEmitter API,會釋放三個事件:

data事件:下一塊數據已經準備好了。
end事件:整個「數據流」處理完了。
error事件:發生錯誤。

使用Promise.race()函數,能夠判斷這三個事件之一誰先發生,只有當data事件最早發生時,才進入下一個數據塊的處理。從而咱們能夠經過一個while循環,完成全部數據的讀取。

const co = require('co');
const fs = require('fs');
const stream = fs.createReadStream('./Les_Miserables.txt');
let valjeanCount = 0;

co(function *() {
    while (true) {
        const res = yield Promise.race([
            new Promise(resolve => stream.once('data', resolve)),
            new Promise(resolve => stream.once('end', resolve)),
            new Promise((resolve, reject) => stream.once('error', reject))
        ]);
        if (!res) {
            break;
        }
        stream.removeAllListeners('data');
        stream.removeAllListeners('end');
        stream.removeAllListeners('error');
        valjeanCount += (res.toString().match(/valjean/ig) || []).length;
    }
    console.log('count:', valjeanCount)
}); 

執行結果以下:

上面代碼採用Stream模式讀取Les_Miserables.txt這個文件,對於每一個數據塊都用stream.once方法,在data,end,error三個事件上添加一次性回調函數。變量res只有在data事件發生時纔有值,而後累加每一個數據塊之中「valjean」這個單詞,能夠看到Les_Miserables.text這個文件中「valjean」這個單詞一共出現了1153次。

8. async函數

8.1 含義

ES2017標準中引入了async函數,使得異步操做變得更加方便。async是Generator函數的語法糖。下面有一個Generator函數,一次讀取兩個文件。

/**
 * Generator函數,依次讀取兩個文件
 */
const fs = require('fs');
const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function (error, data) {
            if (error) return reject(error)
            resolve(data)
        })
    })
}
const gen = function *() {
    const f1 = yield readFile('./a.txt')
    const f2 = yield readFile('./b.txt')
    console.log(f1.toString())
    console.log(f2.toString())
}

function run(gen) {
    var g = gen();
    function next(data) {
        var result = g.next(data);
        if (result.done) return result.value;
        result.value.then(function (data) {
            next(data);
        });
    }
    next();
}
run(gen) 

讀取結果以下:

改寫成async函數,以下:

const fs = require('fs');
const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function (error, data) {
            if (error) return reject(error)
            resolve(data)
        })
    })
}
/**
 * async函數實現讀取兩個文件
 * @returns {Promise<void>}
 */
const asyncReadFile = async function () {
    const f1 = await readFile('./a.txt')
    const f2 = await readFile('./b.txt')
    console.log(f1.toString())
    console.log(f2.toString())
}
asyncReadFile() 

 執行結果以下:

他們執行的結果是同樣的,比較一下能夠看出,async函數其實就是將Generator函數的星號(*)替換成async並放在function關鍵字的前面,函數體內用await代替了yield關鍵字,僅此而已。async函數對Generator函數的改進有四點:

(1)內置執行器
Generator函數的執行必須依靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說async函數的執行,和普通函數同樣,只要調用就好,只要一行。上面代碼中asyncReadFile()這一句就能夠自動執行async函數。這徹底不像Generator函數,須要調用next方法,或者co模塊,才能真正執行,獲得最後結果。

(2)更好的語義
async和await,比起星號和yield,語義更加清楚了。async表示函數裏有異步操做,await表示緊跟在後邊的表達式須要等待結果。

(3)更廣的適用性
co模塊約定,yield命令後面只能是Thunk函數或者Promise對象,而async函數的await命令後面,可使Promise對象和原始的類型的值(數值,字符串,布爾值,可是這是等同於同步操做)
(4)返回的是Promise
async函數返回的是Promise對象,這比Generator函數的返回值是Iterator對象方便多了。能夠用then方法指定下一步操做。

進一步說,async函數徹底能夠看作是多個異步操做,包裝成一個Promise對象,而await命令就是內部then命令的語法糖。

8.2 基本用法

async函數返回一個Promsie對象(嗯嗯,都是返回Promise對象),可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。

下面是一個例子:

    async function getStockPriceByName(name) {
        var symbol = await getStockSymbol(name);
        var stockPrice = await getStockPrice(symbol);
        return stockPrice;
    }

    getStockPriceByName('goog').then(function (result) {
        console.log(result);
    }); 

上面代碼是一個獲取股票的函數,函數前面的async關鍵字表示這個函數內部有異步操做。調用該函數時,會當即返回一個Promise對象。

下面是另一個例子,指定多少毫秒以後輸出一個值。 

    function timeout(ms) {
        return new Promise((resolve) => {
            setTimeout(resolve, ms);
        })
    }

    async function asyncPrint(value, ms) {
        await timeout(ms);
        console.log(value)
    }

    asyncPrint('hello world', 50); 

1. 定義timeout函數,返回一個Promise對象,在ms毫秒以後將Promise對象的狀態改成fullfiled狀態
2. 定義一個async(Generator)函數,async至關於星花,await至關於yield表達式,await表達式後面調用timeout函數,傳入參數ms
3. 調用async函數,傳入兩個參數「hello world」, 50
4. 調用async函數,它自帶執行器,遇到await表達式,執行後面的timeout方法,傳入參數ms;timeout方法返回一個Promise對象,指定ms毫秒以後執行resolve,將Promise對象的狀態修改成fullfiled
5. async方法中偵測到timeout方法執行完畢以後再執行後面的console.log方法,輸出「hello world」,至關於延遲了50毫秒再執行後面的console.log(value)

因爲async函數返回的是Promise對象,能夠做爲await命令的參數。因此上面的代碼也能夠寫成下面的形式

    async function timeout(ms) {
        await new Promise((resolve) => {
            setTimeout(resolve, ms);
        });
    }

    async function asyncPrint(value, ms) {
        await timeout(ms);
        console.log(value);
    }

    asyncPrint('hello world', 50); 

async函數有多種使用形式

    // 函數聲明
    async function foo() {
    }

    // 函數表達式
    const foo = async function () {
    }

    // 對象的方法
    let obj = {async foo()};
    obj.foo().then();

    // class的方法
    class Storage {
        constructor() {
            this.cachePromise = caches.open('avatars');
        }

        async getAvatar(name) {
            const cache = await this.cachePromise;
            return cache.match(`/avatars/${name}.jpg`)
        }
    }
    const storage = new Storage();
    storage.getAvatar('jake').then(...)

    // 箭頭函數
    const foo = async () => {} 

8.3 語法

返回Promise對象

async函數返回一個Promise對象。async函數內部return語句返回的值(done屬性爲false時,value的值),會成爲then方法回調函數的參數。

    async function f() {
        await 'hello'
        return 'world'
    }
    f().then(v => console.log(v)) 

1. 定義async(Generator)函數,async至關於星花,await至關於yield表達式;return語句後面的返回值會變成返回的Promise對象的then語句的參數
2. 調用async方法,自帶執行器,獲得一個Promise對象,調用它的then方法,傳入的參數爲async方法return語句的值,打印這個值,最後輸出「world」

輸出結果以下:

上面代碼中,函數f內部return命令的返回值,會被then方法回調函數接收到。

async函數內部冒出的錯誤,會致使返回的Promise對象爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。

    async function f() {
        throw new Error('出錯了')
    }
    f().then(
        value => console.log(value),
        error => console.log(error)
    ) 

輸出結果以下:

Promise對象的狀態變化

async函數返回的Promise對象,必須等到內部全部的await命令後面的Promise對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數。這一點和Generator函數是不同的,Generator函數是調用指針對象的next方法纔會往下執行。

下面看一個例子: 

    async function getTitle (url) {
        let response = await fetch(url);
        let html = await response.text();
        return html.match(/<title>([\s\S]+)<\/title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log); 

1. 定義async函數(Generator函數)getTitle,async至關於星花,內部await命令至關於yield命令。
2. await命令後面調用fetch方法,抓取網頁,給response賦值
3. await命令後面調用response.text方法,給html賦值
4. return語句返回匹配的標題
5. 調用getTitle函數,只有async函數內部全部的await命令執行完畢纔會調用then方法並將return語句後面的值傳遞給then方法做爲參數,最終打印參數

在網頁中的執行結果以下:

函數getTitle內部有三個操做,抓取網頁,取出文本,匹配頁面標題。只有這三根操做所有完成纔會執行then方法中的console.log。這裏then方法中名沒有傳入參數,可是console.log方法卻直接拿到返回值做爲getTitle方法的返回值做爲參數輸出。

await命令

一般,await命令後面是一個Promise對象,返回該對象的結果。若是不是Promise對象,就直接返回對應的值。

    async function f () {
        return await 123;
    }
    f().then(v => console.log(v)) 

上面代碼中,await命令的參數值是123,這等同於return 123。調用async方法也會等到awa命令執行完,返回123,最後打印123。

另外一種狀況,await命令後面是一個thenable對象(即定義then方法的對象),那麼await會將其等同於Promise對象。

    class Sleep {
        constructor (timeout) {
            this.timeout = timeout
        }

        then (resolve, reject) {
            const startTime = Date.now()
            setTimeout(
                () => resolve(Date.now() - startTime),
                this.timeout
            )
        }
    }

    (async () => {
        const actualTime = await  new Sleep(1000)
        console.log(actualTime)
    })(); 

輸出結果以下:

上面代碼中,await命令後面是一個Sleep對象的實例。這個實例不是Promise對象,可是由於定義了then方法,await會將其視爲Promise處理。

await命令後面的Promise對象若是變成reject狀態,則reject的參數會被catch方法的回調函數接收到。

    async function f () {
        await Promise.reject('出錯了')
    }
    f().then(v => console.log(v)).catch(e => console.log(e)) 

注意,上面代碼中await語句前面沒有return,可是reject方法的參數依然傳入了catch方法的回調函數。這裏若是在await前面加上return,則效果是同樣的。

任何一個await語句後面的Promise對象變成reject狀態,那麼整個async函數都會中斷執行。

    async function f() {
        await Promise.reject('出錯了');
        await Promise.resolve('hello world'); // 不會執行

上面代碼,第二個await語句時不會執行的,由於第一個await語句變成了reject。下面會介紹處理這個reject狀態並繼續往下執行的方法。

有時候咱們但願及時前一個操做失敗,也不要終端後面的異步操做。這時能夠將第一個await放在try...catch裏面,這樣無論這個異步操做是否成功,第二個await都會執行。

    async function f () {
        try {
            await Promise.reject('出錯了');
        } catch (e) {
        }
        return await Promise.resolve('hello world');
    }

    f().then(v => console.log(v)) 

執行結果以下:

上面代碼try代碼塊中await Promise.reject('出錯了')語句雖然會拋出錯誤,可是在catch語句塊中沒有處理,而後繼續執行return await Promise.resolve('hello world'),最後輸出‘hello world’。

另外一種方法是await後面的Promise對象再跟一個catch方法,處理前面可能出現的錯誤。

    async function f () {
        await Promise.reject('出錯了')
            .catch(e => console.log(e));
        return await Promise.resolve('hello world');
    }

    f().then(v => console.log(v)) 

輸出結果以下:

在reject方法後面直接catch方法,處理內部錯誤。外部的then方法處理正常狀況,這樣既能夠處理內部的錯誤,也能夠處理外部的錯誤。

錯誤處理

若是await後面的異步操做出錯,那麼等同於async函數返回的Promise對象被reject。

    async function f () {
        await new Promise(function (resolve, reject) {
            throw new Error('出錯了')
        })
    }
    f().then(v => console.log(v)).catch(e => console.log(e)) 

執行結果以下:

 

上面代碼中,async函數f執行後,await後面的Promise對象會拋出一個錯誤對象,致使catch方法的回調函數被調用,它的參數就是拋出的錯誤對象。

防止錯誤拋出的方法,也是將其放在try...catch代碼塊中,吃掉錯誤。

    async function f () {
        try {
            await new Promise(function (resolve, reject) {
                throw new Error('出錯了')
            })
        } catch (e) {
        }
        return await ('hello world')
    }
    f().then(value => console.log(value)).catch(e => console.log(e)) 

輸出結果以下:

若是有多個await命令,能夠統一放在try...catch結構中。

    async function main () {
        try {
            const val1 = await firstStep();
            const val2 = await secondStep(val1);
            const val3 = await thirdStep(val1, val2);
            console.log('final: ' val3);
        } catch (e) {
            console.log(e)
        }        
    } 

下面的例子,使用try...catch解構,實現多3次重複嘗試。

const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test () {
    let i;
    for (i = 0; i < NUM_RETRIES; i++) {
        try {
            await superagent.get('http://google.com/this-throws-an-error');
            break
        } catch (e) {
        }
    }
    console.log(i)
}
test(); 

輸出結果以下:

上面代碼中,若是await操做成功,就會使用break語句退出循環;不然會被catch語句捕捉,進入下一輪循環。

使用注意點

第一點,前面已經說過,await命令後面的Promise對象,運行結果多是rejected,因此最好把await命令放在try...catche代碼塊中。 

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另外一種寫法

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}

 第二點,多個await命令後面的異步操做,若是不存在繼發關係,最好讓他們同時觸發。

let foo = await getFoo();
let bar = await getBar(); 

上面代碼中,getFoo,getBar是連個獨立的異步操做(相互不依賴),被寫成繼發關係。這樣比較耗時,由於只有getFoo完成之後,纔會執行getBar,徹底可讓他們同時觸發。

    let foo = await getFoo();
    let bar = await getBar();
    
    // 寫法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 寫法二,異步方法同步執行
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;

第三點, await命令只能用在async函數中,若是用在普通函數中,就會報錯。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 報錯
  docs.forEach(function (doc) {
    await db.post(doc);
  });
} 

上面代碼會報錯,由於await用在普通函數中。可是若是將forEach方法的參數改爲async也是有問題的。 

function dbFuc(db) { //這裏不須要 async
  let docs = [{}, {}, {}];

  // 可能獲得錯誤結果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
} 

上面代碼不會正常工做,緣由是這時三個db.post操做是併發執行的,也是同步執行,而不是繼發執行。正確的寫法是使用for...of循環。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
} 

若是確實但願多個請求併發執行,可使用Promise.all方法。當三個請求都會resolved的時候,下面的兩種寫法效果相同。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的寫法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
} 

目前,esm模塊加載器支持頂層await,即await命令能夠不放在async函數裏面,直接使用。

// async 函數的寫法
const start = async () => {
  const res = await fetch('google.com');
  return res.text();
};

start().then(console.log);

// 頂層 await 的寫法
const res = await fetch('google.com');
console.log(await res.text()); 

上面代碼中,第二種寫法的腳本必須使用esm加載器,纔會生效。

第四點,async函數能夠保留運行堆棧。

const a = () => {
  b().then(() => c());
}; 

上面代碼中給,函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。等到b()運行結束,可能a()早已經雲心剛結束了。b()所在的上下文環境已經消失。若是b()或者c()報錯,錯誤堆棧將不包括a().

將這個例子改造一下以下:

const a = async () => {
  await b();
  c();
}; 

上面代碼中,b()運行的時候,a()是暫停執行,上下文環境保存着,一旦b()或c()報錯,錯誤堆棧將包括a()。

8.4 async函數的實現原理

async函數的實現原理,就是將Generator函數和自動執行器,包裝在一個函數裏。

async function fn(args) {
  // ...
}

// 等同於

function fn(args) {
  return spawn(function* () {
    // ...
  });
} 

全部的async函數均可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。下面給出spaw函數的實現,基本就是前文自動執行器的翻版。

    function spawn (genF) {
        return new Promise(function (resolve, reject) {
            const gen = genF()

            function step (nextF) {
                let next;
                try {
                    newxt = nextF()
                } catch (e) {
                    return reject(e)
                }
                if (next.done) {
                    return resolve(next.value)
                }
                Promise.resolve(next.value).then(function (v) {
                    step(function () {
                        return gen.next(v)
                    })
                }, function (e) {
                    step(function () {
                        return gen.throw(e)
                    })
                })
            }

            step(function () {
                return gen.next(undefined)
            })
        })
    } 

8.5 與其餘異步處理方法的比較

咱們經過一個例子來看看async與Promise,Generator函數的比較。

首先是Promise的寫法:

    function chainAnimationsPromise (elem, animations) {

        // 變量ret用來保存上一個動畫的返回值
        let ret = null

        // 新建一個空的Promise
        let p = Promise.resolve()

        // 使用then方法,添加全部動畫
        for (let anim of animations) {
            p = p.then(function (val) {
                ret = val
                return anim(elem)
            })
        }

        // 返回一個部署了錯誤捕捉機制的Promise
        return p.catch(function (e) {
            // 忽略錯誤,繼續執行
        }).then(function () {
            return ret
        })
    } 

雖然Promise的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是Promise的API(then,catch),操做自己的語義反而不容易看出來。

接着是Generator函數的寫法。

    function chainAnimationsGenerator (elem, animations) {
        return spawn(function *() {
            let ret = null
            try {
                for (let anim of animations) {
                    ret = yield anim(elem)
                }
            } catch (e) {
                //
            }
            return ret
        })
    } 

上面代碼使用Generator函數遍歷了每一個動畫,語義比Promise寫法更加清晰,用戶定義的操做所有都出如今spawn函數的內部。這個寫法問題在於,必須有一個任務運行器,自動執行Generator函數,上面代碼中spawn函數就是自動執行器,它返回一個Promise對象,並且必須保證yield語句後面的表達式,必須返回一個Promise。

最後是yield函數的寫法。

    async function chianAnimationsAsync(elem, animations) {
        let ret = null
        try {
            for (let anim of animations) {
                ret = await anim(elem)
            }
        } catch (e) {
            // 忽略錯誤,繼續執行
        }
        return ret
    } 

能夠看到async函數的實現最簡潔,最符合語義,幾乎沒有語義不想管的代碼。它將Generator寫法中的的自動執行器,改在語言層面提供,不暴露給用戶,所以代碼量最少。若是使用Generator寫法,自動執行器須要用戶本身提供。

8.6 按順序完成異步操做

實際開發中,常常遇到一組異步操做,須要按順序執行。好比讀取一組URL地址,而後按照讀取順序輸出結果。

Promise的寫法以下:

    function logInOrder (urls) {
        // 遠程讀取全部的URL
        const textPromises = urls.map(url => {
            return fetch(url).then(response => response.text())
        })
        
        // 按次序輸出
        textPromises.reduce((chain, textPromise) => {
            return chain.then(() => textPromise).then(text => console.log(text))
        }, Promise.resolve())
    } 

上面代碼使用fetch方法,同時遠程讀取一組URL。每一個fetch操做都返回一個Promise對象,放入textPromises數組。而後,reduce方法一次處理每一個Promise對象,放入textPromise數組。而後,reduce方法一次處理每一個Promise對象,而後使用then,將全部Promise對象鏈接起來,所以就能夠以此輸出結果。

這種寫法不直觀,還要使用數組對象的reduce方法,可讀性比較差,下面是async函數的實現。

    async function logInOrder (urls) {
        for (let url of urls) {
            const response = await fetch(url)
            console.log(await  response.text())
        }
    } 

上面代碼大大簡化,問題是全部的遠程操做都是繼發。只有前一個RUL返回結果,纔會去讀下一個URL,這樣的效率不好,浪費時間,咱們須要併發發出遠程請求,結果按照前後順序輸出就行了。

    async function loadInOrder (urls) {
        // 併發讀取遠程URL
        const textPromises = urls.map(async url => {
            const response = await fetch(url)
            return response.text()
        })
        // 按次序輸出
        for (const textPromise of textPromises) {
            console.log(await textPromise)
        }
    } 

上面代碼中,雖然map方法的參數是async函數,可是他們是併發執行的,由於只有async函數內部是繼發執行,外部不受影響。後面的for...of循環內部使用了await,所以實現了按順序輸出。

8.7 異步遍歷

Iterator接口是一種數據遍歷的協議,只要調用遍歷器對象的next方法,就會獲得一個指針對象,表示當前遍歷指針所在的那個位置的信息。next方法返回的對象和Generator的next方法返回的對象是同樣的{value: '', done: ''},其中value表示當前的數據的值,done是一個布爾值,表示遍歷是否結束。

這裏隱藏着一個規定,next方法必須是同步的,只要調用就必須馬上返回該值。這就是說,一旦執行next方法,就必須同步地獲得value和done這兩個屬性。若是遍歷指針正好指向同步操做,這是沒有問題的,可是對於異步操做,就不太合適了。目前解決的方法是Generator函數裏的異步操做,返回一個Thunk函數或者Promise對象,即value是一個Thunk函數或者Promise對象,等待之後返回真正的值,done屬性則仍是同步產生的。

ES2018引入「異步遍歷器」(Async Iterator),爲異步操做提供原生的遍歷器接口,即value和done屬性都是異步產生的。

異步遍歷的接口

異步遍歷器的最大的語法特色,就是調用遍歷器的next方法,返回的是一個Promise對象。

asyncIterator
  .next()
  .then(
    ({ value, done }) => /* ... */
  ); 

上面示例代碼中asyncIterator是一個異步遍歷器,調用next方法之後,返回一個Promise對象。所以可使用then方法指定,這個Promise對象的狀態變成resolved的回調函數。回調函數的參數以一個有value和done屬性的對象,這個跟同步遍歷器是同樣的。

一個對象的同步遍歷器的接口部署在Symbo.iterator屬性上。一樣的異步遍歷器接口部署在Symbol.asyncIterator屬性上面。無論是什麼樣的對象,只要它的Symbol.asyncIterator屬性有值,就表示能夠對它進行異步遍歷。

下面是一個異步遍歷器的示例代碼:

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

asyncIterator
.next()
.then(iterResult1 => {
  console.log(iterResult1); // { value: 'a', done: false }
  return asyncIterator.next();
})
.then(iterResult2 => {
  console.log(iterResult2); // { value: 'b', done: false }
  return asyncIterator.next();
})
.then(iterResult3 => {
  console.log(iterResult3); // { value: undefined, done: true }
}); 

代碼中,異步遍歷器其實返回了兩次值。第一次調用的時候,返回一個Promise對象,等到Promise對象resolve了,再返回一個表示當前數據成員信息的對象,這就是說,異步遍歷器與同步遍歷器的行爲是一致的,只是會先返回Promise對象做爲中介。

因爲異步遍歷器的next方法,返回的是一個Promise對象。所以,能夠把它放在await命令後面。

async function f() {
  const asyncIterable = createAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { value: 'a', done: false }
  console.log(await asyncIterator.next());
  // { value: 'b', done: false }
  console.log(await asyncIterator.next());
  // { value: undefined, done: true }

上面代碼中,next方法用await處理後,就沒必要使用then方法了。這個流程已經很接近同步處理了。

注意,異步遍歷器的next方法是能夠連續調用的,沒必要等到上一步產生的Promise對象resolve之後再調用。這種狀況下,next方法會累積起來,自動按照每一步的順序運行下去。下面是一個例子,把全部的next方法放在Promise.all方法裏面。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
  asyncIterator.next(), asyncIterator.next()
]);

console.log(v1, v2); // a b 

另一種方法是一次性調用全部的next方法,而後await最後一步操做。

async function runner() {
  const writer = openFile('someFile.txt');
  writer.next('hello');
  writer.next('world');
  await writer.return();
}

runner(); 

for await of

上面說過,for...of循環用於遍歷同步的Iterator接口。新引入的for await ... of循環,則是調用遍歷異步的Iteator接口。

async function f() {
  for await (const x of createAsyncIterable(['a', 'b'])) {
    console.log(x);
  }
}
// a
// b 

上面代碼中,creatAsyncIterator()返回一個擁有異步遍歷器接口的對象,for...of循環自動調用這個對象的異步遍歷器的next方法,會獲得一個Promise對象。await用來處理這個Promise對象,一旦resolve,就會把獲得的值x傳入for....of循環體。

for await...of循環的一個用途,是部署了asyncIterable操做的異步操做,能夠直接放在這個循環體裏。

let body = '';

async function f() {
  for await(const data of req) body += data;
  const parsed = JSON.parse(body);
  console.log('got', parsed);
} 

上面代碼中,req是一個asyncIterable對象,用來異步讀取數據。能夠看到,使用for await...of循環後,代碼很是簡潔。

若是next方法返回的Promise對象被reject,for await...of就會報錯,要用try...catch捕獲。

async function () {
  try {
    for await (const x of createRejectingIterable()) {
      console.log(x);
    }
  } catch (e) {
    console.error(e);
  }
} 

注意,for await...of循環也能夠用於同步遍歷器。

(async function () {
  for await (const x of ['a', 'b']) {
    console.log(x);
  }
})(); 

Node v10支持異步遍歷器,node中的Stream模塊就部署了這個接口,下面是讀取文件的傳統寫法和異步遍歷器的寫法的差別。

// 傳統寫法
function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );
  readStream.on('data', (chunk) => {
    console.log('>>> '+chunk);
  });
  readStream.on('end', () => {
    console.log('### DONE ###');
  });
}

// 異步遍歷器寫法
async function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );

  for await (const chunk of readStream) {
    console.log('>>> '+chunk);
  }
  console.log('### DONE ###');
} 

異步Generator函數

就像Generator函數返回一個同步遍歷器對象同樣,異步Generator函數的做用,是返回一個異步遍歷器對象。

在語法上,異步Generator函數就是async函數與Generator函數的結合。

    async function* gen() {
        yield 'hello';
    }
    const genObj = gen();
    genObj.next().then(x => console.log(x)); 

代碼輸出結果以下

上面代碼中,gen是一個異步Generator函數,執行後返回一個異步Iterator對象。改對象調用next方法,返回一個Promise對象。

異步遍歷器的設計目的之一,就是Generaotr函數處理同步和異步操做的時候,可以使用同一套接口。

// 同步 Generator 函數
function* map(iterable, func) {
  const iter = iterable[Symbol.iterator]();
  while (true) {
    const {value, done} = iter.next();
    if (done) break;
    yield func(value);
  }
}

// 異步 Generator 函數
async function* map(iterable, func) {
  const iter = iterable[Symbol.asyncIterator]();
  while (true) {
    const {value, done} = await iter.next();
    if (done) break;
    yield func(value);
  }
} 

上面代碼中,map是一個Generator函數,第一個參數是可遍歷對象iterator,第二個參數是一個回調函數func。map的做用是將iterator每一步返回的值,用func進行處理。上面有兩個版本的map,前一個處理同步遍歷器,後一個處理異步遍歷器。能夠看到連個版本的寫法基本一致。

下面是一個異步Generator函數的例子。

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
} 

上面代碼中,異步操做前面使用await關鍵字標明,await後面的操做應該返回Promise對象。凡是shiyongyield關鍵字的地方,就是next方法停下來的地方,它後面的表達式的值(即await file.readLine()的值),會做爲next()返回對象的value屬性,這一點是與同步Generator函數一致的。

異步Generator函數內部,可以同時使用await和yield命令。能夠這樣理解,await命令用於將外部操做產生的值輸入函數內部,yield命令用於將函數內部的值輸出。

上面代碼定義的異步Generator函數的用法以下:

(async function () {
  for await (const line of readLines(filePath)) {
    console.log(line);
  }
})() 

異步Generator函數能夠與for await...of循環結合起來使用。

async function* prefixLines(asyncIterable) {
  for await (const line of asyncIterable) {
    yield '> ' + line;
  }
} 

異步Generator函數的返回值是一個異步Iterator,即每次調用它的next方法,會返回一個Promise對象,也就是說,跟在yield命令後面的,應該是一個Promise對象。若是想上面的那個例子那樣,yield命令後面是一個字符串,會被自動包裝成一個Promise對象。

function fetchRandom() {
  const url = 'https://www.random.org/decimal-fractions/'
    + '?num=1&dec=10&col=1&format=plain&rnd=new';
  return fetch(url);
}

async function* asyncGenerator() {
  console.log('Start');
  const result = await fetchRandom(); // (A)
  yield 'Result: ' + await result.text(); // (B)
  console.log('Done');
}

const ag = asyncGenerator();
ag.next().then(({value, done}) => {
  console.log(value);
}) 

執行順序以下:

1. ag.next()馬上返回一個Promise對象
2. asyncGenerator函數開始執行,打印Start
3. await命令返回一個Promise對象,asyncGenerator函數暫停在這裏
4. A處變成fulfilled狀態,產生的值放入result變量,asyncGenerator函數繼續往下執行
5. 函數在B處的yield暫停執行,一旦yield命令取到值,ag.next()返回的那個Promise對象編程fulfilled狀態
6. ag.next()後面的then方法指定的回調函數開始執行。改回調函數的參數是一個對象{value, done},其中value的值是yield命令後面的那個表達式的值,done的值是false

執行結果以下

A和B兩行的做用相似下面的代碼

return new Promise((resolve, reject) => {
  fetchRandom()
  .then(result => result.text())
  .then(result => {
     resolve({
       value: 'Result: ' + result,
       done: false,
     });
  });
}); 

若是 一部Generator函數拋出錯誤會致使Promise對象的狀態變爲reject,而後拋出的錯誤被catch方法捕獲。

    async function* asyncGenerator() {
        throw new Error('Problem!');
    }

    asyncGenerator().next().catch(err => console.log(err)); 

執行結果以下:

注意,普通的async函數返回的是一個Promise對象,而異步Generator函數返回的是一個異步Iterator對象。能夠這樣理解,async函數和異步Generator函數,是封裝異步操做的兩種方法,都用來達到同一種目的。區別在於,前者自帶執行器,後者經過for await...of執行,或者能夠本身編寫執行器。下面是一個異步Generator函數的執行器。

    // 異步執行器
    async function takeAsync(asyncIterable, count = Infinity) {
        const result = [];
        const iterator = asyncIterable[Symbol.asyncIterator]();
        while (result.length < count) {
            const {value, done} = await iterator.next();
            if (done) break;
            result.push(value);
        }
        return result;
    }

    // 使用異步執行器
    async function f() {
        async function* gen() {
            yield 'a';
            yield 'b';
            yield 'c';
        }

        return await takeAsync(gen());
    }

    f().then(function (result) {
        console.log(result); // ['a', 'b', 'c']
    }) 

執行結果以下:

上面代碼中,異步Generator函數產生的異步遍歷器,會經過while循環自動執行,每當await iterator.next()完成,就會進入下一輪循環。一旦done屬性變成true,就會調出循環,異步遍歷器執行結束。

異步Generator函數出現之後,JavaScript就有了四種形式的函數:普通函數async函數Generator函數異步Generator函數。一般,若是是一系列按照順序執行的異步操做(好比讀取文件,而後寫入新內容,再存入硬盤)可使用async函數;若是是一系列產生相同數據結構的異步操做(好比一行一行的讀取文件),可使用異步Generator函數。

異步Generator函數也能夠經過next方法的參數,接收外部傳入的數據。

const writer = openFile('someFile.txt');
writer.next('hello'); // 當即執行
writer.next('world'); // 當即執行
await writer.return(); // 等待寫入結束 

上面代碼中,openFile是一個異步Generator函數。next方法的參數,向該函數內部的操做傳入數據。每次next方法都是同步執行的,最後的await命令用於等待整個操做結束。

最後,同步的數據結構,也可使用異步Generator函數。

async function* createAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem;
  }
} 

上面代碼中,因爲沒有異步操做,因此也就沒有使用await關鍵字。

yield * 語句

yield*語句也能夠跟一個異步遍歷器。

    async function* gen1() {
        yield 'a';
        yield 'b';
        return 2;
    }

    async function* gen2() {
        // result 最終會等於 2
        const result = yield* gen1();
    }
    (async function () {
        for await (const x of gen2()) {
            console.log(x);
        }
    })(); 

上面代碼中,gen2函數裏的result變量,最後的值是2.

與同步Generator函數同樣,for await...of循環會展開yield*,輸出結果以下:

相關文章
相關標籤/搜索