深入淺出ES6(十一):生成器 Generators,續篇

快速回顧

在第三篇文章中,我們着重講解了生成器的基本行爲。你可能對此感到陌生,但是並不難理解。生成器函數與普通函數有很多相似之處,它們之間最大的不同是,普通函數一次執行完畢,而生成器函數體每次執行一部分,每當執行到一個yield表達式的時候就會暫停。

儘管在那篇文章中我們進行過詳細解釋,但我們始終未把所有特性結合起來給大家講解示例。現在就讓我們出發吧!

    function* somewords() {
      yield "hello";
      yield "world";
    }
    for (var word of somewords()) {
      alert(word);
    }

這段腳本簡單易懂,但是如果你把代碼中不同的比特位當做戲劇中的任務,你會發現它變得如此與衆不同。穿上新衣的代碼看起來是這樣的:


(譯者注:下面這是原作者創作的一個劇本,他將ES6中的各種函數和語法擬人化,以講解生成器(Generator)的實現原理)

場景 - 另一個世界的計算機,白天

for loop女士獨自站在舞臺上,戴着一頂安全帽,手裏拿着一個筆記板,上面記載着所有的事情。

                  for loop:
                (電話響起)
                somewords()!

generator出現:這是一位高大的、有着一絲不苟紳士外表的黃銅機器人。
它看起來足夠友善,但給人的感覺仍然是冷冰冰的金屬。

                  for loop:
            (瀟灑地拍了拍她的手)
          好吧!我們去找些事兒做吧。
             (對generator說)
                  .next()!

generator動了起來,就像突然擁有了生命。

                 generator:
       {value: "hello", done: false}

然而猝不及防的,它以一個滑稽的姿勢停止了動作。

                  for loop:
                   alert!

alert小子飛快衝進舞臺,眼睛大睜,上氣不接下氣。我們感覺的到他一向如此。

                  for loop:
             對user說“hello”。

alert小子轉身衝下舞臺。

                   alert:
            (舞臺下,大聲尖叫)
               一切都靜止了!
             你正在訪問的頁面說,
                  “hello”!

停留了幾秒鐘後,alert小子跑回舞臺,穿過所有人滑停在for loop女士身邊。

                   alert:
                 user說ok。
                  for loop:
           (瀟灑地拍了拍她的手)
          好吧!我們去找些事兒做吧。
           (回到generator身邊)
                  .next()!

generator又一次煥發生機。

                 generator:
       {value: "world", done: false}

它換了個姿勢又一次凍結。

                 for loop:
                   alert!
                   alert:
                (已經跑起來)
                  正在搞定!
              (舞臺下,大聲尖叫)
                一切都靜止了!
              你正在訪問的頁面說,
                  “world”!

又一次暫停,然後alert突然跋涉回到舞臺,垂頭喪氣的。

                   alert:
            user再一次說ok,但是…
              但是請阻止這個頁面
               創建額外的對話。

他噘着嘴離開了。

                  for loop:
            (瀟灑地拍了拍她的手)
           好吧!我們去找些事兒做吧。
            (回到generator身邊)
                  .next()!

generator第三次煥發生機。

                 generator:
                 (莊嚴的)
       {value: undefined, done: true}

它的頭低下了,光芒從它的眼裏消失。它不再移動。

                  for loop
               我的午餐時間到了。

她離開了。

一會兒,garbage collector(垃圾收集器)老頭進入,撿起了奄奄一息的generator,將它帶下舞臺。

好吧,這一齣戲不太像哈姆雷特,但你應該可以想象得出來。


好吧,這一齣戲不太像哈姆雷特,但你應該可以想象得出來。

正如你在戲劇中看到的,當生成器對象第一次出現時,它立即暫停了。每當調用它的.next()方法,它都會甦醒並向前執行一部分。

所有動作都是單線程同步的。請注意,無論何時永遠只有一個真正活動的角色,角色們不會互相打斷,亦不會互相討論,他們輪流講話,只要他們的話沒有說完都可以繼續說下去。(就像莎士比亞一樣!)

每當for-of循環遍歷生成器時,這齣戲的某個版本就展開了。這些.next()方法調用序列永遠不會在你的代碼的任何角落出現,在劇本里我把它們都放在舞臺上了,但是對於你和你的程序而言,所有這一切都應該在幕後完成,因爲生成器和for-of循環就是被設計成通過迭代器接口聯結工作的。

所以,總結一下到目前爲止所有的一切:

  • 生成器對象是可以產生值的優雅的黃銅機器人。
  • 每個生成器函數體構成的單一代碼塊就是一個機器人。

如何關停生成器

我在第1部分沒有提到這些繁瑣的生成器特性:

  • generator.return()
  • generator.next()的可選參數
  • generator.throw(error)
  • yield*

如果你不理解這些特性存在得意義,就很難對它們提起興趣,更不用說理解它們的實現細節,所以我選擇直接跳過。但是當我們深入學習生成器時,勢必要仔細瞭解這些特性的方方面面。

你或許曾使用過這樣的模式:

    function dothings() {
      setup();
      try {
        // ... 做一些事情
      } finally {
        cleanup();
      }
    }
    dothings();

清理(cleanup)過程包括關閉連接或文件,釋放系統資源,或者只是更新dom來關閉“運行中”的加載動畫。我們希望無論任務成功完成與否都觸發清理操作,所以執行流入到finally代碼塊。

那麼生成器中的清理操作看起來是什麼樣的呢?

    function* producevalues() {
      setup();
      try {
        // ... 生成一些值
      } finally {
        cleanup();
      }
    }
    for (var value of producevalues()) {
      work(value);
    }

這段代碼看起來很好,但是這裏有一個問題:我們沒在try代碼塊中調用work(value),如果它拋出異常,我們的清理步驟會如何執行呢?

或者假設for-of循環包含一條break語句或return語句。清理步驟又會如何執行呢?

放心,清理步驟無論如何都會執行,ES6已經爲你做好了一切。

我們第一次討論迭代器和for-of循環時曾說過,迭代器接口支持一個可選的.return()方法,每當迭代在迭代器返回{done:true}之前退出都會自動調用這個方法。生成器支持這個方法,mygenerator.return()會觸發生成器執行任一finally代碼塊然後退出,就好像當前的生成暫停點已經被祕密轉換爲一條return語句一樣。

注意,.return()方法並不是在所有的上下文中都會被自動調用,只有當使用了迭代協議的情況下才會觸發該機制。所以也有可能生成器沒執行finally代碼塊就直接被垃圾回收了。

如何在舞臺上模擬這些特性?生成器被凍結在一個需要一些配置的任務(例如,建造一幢摩天大樓)中間。突然有人拋出一個錯誤!for循環捕捉到這個錯誤並將它放置在一遍,她告訴生成器執行.return()方法。生成器冷靜地拆除了所有腳手架並停工。然後for循環取回錯誤,繼續執行正常的異常處理過程。

生成器主導模式

到目前爲止,我們在劇本中看到的生成器(generator)和使用者(user)之間的對話非常有限,現在換一種方式繼續解釋:

在這裏使用者主導一切流程,生成器根據需要完成它的任務,但這不是使用生成器進行編程的唯一方式。

在第1部分中我曾經說過,生成器可以用來實現異步編程,完成你用異步回調或promise鏈所做的一切。我知道你一定想知道它是如何實現的,爲什麼yield的能力(這可是生成器專屬的特殊能力)足夠應對這些任務。畢竟,異步代碼不僅產生(yield)數據,還會觸發事件,比如從文件或數據庫中調用數據,向服務器發起請求並返回事件循環來等待異步過程結束。生成器如何實現這一切?它又是如何不借助回調力量從文件、數據庫或服務器中接受數據?

爲了開始找出答案,考慮一下如果.next()的調用者只有一種方法可以傳值返回給生成器會發生什麼?僅僅是這一點改變,我們就可能創造一種全新的會話形式:

事實上,生成器的.next()方法接受一個可選參數,參數稍後會作爲yield表達式的返回值出現在生成器中。那就是說,yield語句與return語句不同,它是一個只有當生成器恢復時纔會有值的表達式。

    var results = yield getdataandlatte(request.areacode);

這一行代碼完成了許多功能:

  • 調用getdataandlatte(),假設函數返回我們在截圖中看到的字符串“get me the database records for area code...”。
  • 暫停生成器,生成字符串值。
  • 此時可以暫停任意長的時間。
  • 最終,直到有人調用.next({data: ..., coffee: ...}),我們將這個對象存儲在本地變量results中並繼續執行下一行代碼。

下面這段代碼完整地展示了這一行代碼完整的上下文會話:

    function* handle(request) {
      var results = yield getdataandlatte(request.areacode);
      results.coffee.drink();
      var target = mosturgentrecord(results.data);
      yield updatestatus(target.id, "ready");
    }

yield仍然保持着它的原始含義:暫停生成器,返回值給調用者。但是確實也發生了變化!這裏的生成器期待來自調用者的非常具體的支持行爲,就好像調用者是它的行政助理一樣。

普通函數則與之不同,通常更傾向於滿足調用者的需求。但是你可以藉助生成器創造一段對話,拓展生成器與其調用者之間可能存在的關係。

這個行政助理生成器運行器可能是什麼樣的?它大可不必很複雜,就像這樣:

    function rungeneratoronce(g, result) {
      var status = g.next(result);
      if (status.done) {
        return;  // phew!
      }
      // 生成器請我們去獲取一些東西並且
      // 當我們搞定的時候再回調它
      doasynchronousworkincludingespressomachineoperations(
        status.value,
        (error, nextresult) => rungeneratoronce(g, nextresult));
    }

爲了讓這段代碼運行起來,我們必須創建一個生成器並且運行一次,像這樣:

      rungeneratoronce(handle(request), undefined);

在之前的文章中,我一個庫的示例中提到Q.async(),在那個庫中,生成器是可以根據需要自動運行的異步過程。rungeneratoronce正式這樣的一個具體實現。事實上,生成器一般會生成Promise對象來告訴調用者要做的事情,而不是生成字符串來大聲告訴他們。

如果你已經理解了Promise的概念,現在又理解了生成器的概念,你可以嘗試修改rungeneratoronce的代碼來支持Promise。這個任務不簡單,但是一旦成功,你將能夠用Promise線性書寫複雜的異步算法,而不僅僅通過.then()方法或回調函數來實現異步功能。

如何銷燬生成器

你是否有看到rungeneratoronce的錯誤處理過程?答案一定是沒有,因爲上面的示例中直接忽略了錯誤!

是的,那樣做不好,但是如果我們想要以某種方法給生成器報告錯誤,可以嘗試一下這個方法:當有錯誤產生時,不要繼續調用generator.next(result)方法,而應該調用generator.throw(error)方法來拋出yield表達式,進而像.return()方法一樣終止生成器的執行。但是如果當前的生成暫停點在一個try代碼塊中,那麼會catch到錯誤並執行finally代碼塊,生成器就恢復執行了。

另一項艱鉅的任務來啦,你需要修改rungeneratoronce來確保.throw()方法能夠被恰當地調用。請記住,生成器內部拋出的異常總是會傳播到調用者。所以無論生成器是否捕獲錯誤,generator.throw(error)都會拋出error並立即返回給你。

當生成器執行到一個yield表達式並暫停後可以實現以下功能:

  • 調用generator.next(value),生成器從離開的地方恢復執行。
  • 調用generator.return(),傳遞一個可選值,生成器只執行finally代碼塊並不再恢復執行。
  • 調用generator.throw(error),生成器表現得像是yield表達式調用一個函數並拋出錯誤。
  • 或者,什麼也不做,生成器永遠保持凍結狀態。(是的,對於一個生成器來說,很可能執行到一個try代碼塊,永不執行finally代碼塊。這種狀態下的生成器可以被垃圾收集器回收。)

看起來生成器函數與普通函數的複雜度相當,只有.return()方法顯得不太一樣。

事實上,yield與函數調用有許多共通的地方。當你調用一個函數,你就暫時停止了,對不對?你調用的函數取得主導權,它可能返回值,可能拋出錯誤,或者永遠循環下去。

結合生成器實現更多功能

我再展示一個特性。假設我們寫一個簡單的生成器函數聯結兩個可迭代對象:

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

es6支持這樣的簡寫方式:

    function* concat(iter1, iter2) {
      yield* iter1;
      yield* iter2;
    }

普通yield表達式只生成一個值,而yield*表達式可以通過迭代器進行迭代生成所有的值。

這個語法也可以用來解決另一個有趣的問題:在生成器中調用生成器。在普通函數中,我們可以從將一個函數重構爲另一個函數並保留所有行爲。很顯然我們也想重構生成器,但我們需要一種調用提取出來的子例程的方法,我們還需要確保,子例程能夠生成之前生成的每一個值。yield*可以幫助我們實現這一目標。

    function* factoredoutchunkofcode() { ... }
    function* refactoredfunction() {
      ...
      yield* factoredoutchunkofcode();
      ...
    }

考慮一下這樣一個場景:一個黃銅機器人將子任務委託給另一個機器人,函數對組織同步代碼來說至關重要,所以這種思想可以使基於生成器特性的大型項目保持簡潔有序。

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