<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      JavaScript中的Generator函數

      1. 簡介

      Generator函數時ES6提供的一種異步編程解決方案。Generator語法行為和普通函數完全不同,我們可以把Generator理解為一個包含了多個內部狀態的狀態機

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

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

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

          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}

      注意yield表達式后面的表達式,只有當調用next方法,內部指針指向該語句時才會執行,相當于JavaScript提供了手動的“惰性求值”語法功能。

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

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

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

      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*,輸出結果如下:

       

      posted @ 2018-12-07 12:59  nd  閱讀(5443)  評論(2)    收藏  舉報
      主站蜘蛛池模板: 人人妻人人狠人人爽天天综合网| 国产精品av中文字幕| 激情综合网激情综合| 欧美午夜成人片在线观看| 色综合伊人色综合网站| 亚欧乱色国产精品免费九库| 激情国产一区二区三区四区| 国产热A欧美热A在线视频| 婷婷色综合成人成人网小说| 亚洲国产v高清在线观看| 亚洲高清日韩专区精品| 绝顶丰满少妇av无码| 久热这里只精品视频99| 亚洲精品一区二区五月天| 日韩欧美一中文字暮专区| 亚洲av成人无网码天堂| 免费视频一区二区三区亚洲激情| 中文字幕日韩一区二区不卡| 中文字幕在线亚洲精品| 人人综合亚洲无线码另类| 午夜国产小视频| 久久国内精品自在自线91| 亚洲高清aⅴ日本欧美视频| 久99久热只有精品国产99| 国产美女高潮流白浆视频| 黑人精品一区二区三区不| 久操资源站| 国产一区二区日韩在线| 午夜福利免费视频一区二区| 影音先锋大黄瓜视频| 色综合一本到久久亚洲91| 狠狠v日韩v欧美v| 国产亚洲av产精品亚洲| 高清有码国产一区二区| 中文国产人精品久久蜜桃| 亚洲一区二区精品极品| 无码A级毛片免费视频下载| 精品久久久bbbb人妻| 久久婷婷大香萑太香蕉AV人| 国产精品黄色片在线观看| 国产精品中文字幕综合|