ES6 Generators 工做原理

文章摘要

Generator 是 ES6 添加的一個特性,容許函數的暫停和恢復,本文使用 generator 構建了一個惰性隊列,並分析其原理。javascript

文章正文

編寫正確運行的軟件多是困難的,可是咱們知道這僅僅是挑戰的開始。對一個好的解決方案建模能夠把編程從 「能夠運行」 轉換到 「這樣作更好」。相對的,有些事就像下面的註釋同樣:前端

//
//親愛的代碼維護者:
// 
//一旦你試圖優化代碼,
//而且最終意識到這是一個錯誤的決定,
//請把下面的變量加一,
//來警告下一個傢伙
//
//total_hours_wasted_here = 42
//

(源自: http://stackoverflow.com/ques...java

今天,咱們會去探索 ES6 Generators 的工做原理,從而更好的理解它並用新的方式來解決一些老的問題。node

非阻塞

你可能已經聽過編寫非阻塞 javascript 代碼的重要性。當咱們處理 I/O 操做,好比發送 HTTP 請求或者寫數據庫,咱們一般都會使用回調或者 promises。阻塞代碼會凍結整個應用,在絕大多數場景下都不是一個能夠被使用的方案。webpack

這樣作的另一個後果是,若是你寫了一段無限循環的 javascript 代碼,好比git

node -e 'while(true) {}'

它將極可能凍結你的電腦而且須要系統重啓,請不要在家嘗試。github

考慮到這些,當聽到 ES6 Generators 容許咱們在函數的中間暫停執行而後在將來的某個時候恢復執行時,感到很是好奇。
雖然有些工具好比 Regenerator 和 Babel 已經把這些特性部署到了 ES5。你是否困惑過它們是怎樣作到這些的?今天,咱們會找到真相。
但願咱們能夠更深刻的理解 generators, 更好的發揮它的做用。web

一個惰性序列

讓咱們從一個簡單的例子開始。好比你要操做一個序列,你可能會建立一個數組而且按照數組的方式操做其值。可是若是這個序列是無限長的呢?數組就不行了,咱們可使用 generator 函數來作:數據庫

function* generateRandoms (max) {
  max = max || 1;

  while (true) {
    let newMax = yield Math.random() * max;
    if (newMax !== undefined) {
      max = newMax;
    }
  }
}

注意 function* 部分,它標示這是一個 「generator 函數」,而且表現與普通函數不一樣。另外一個重要的部分是 yield 關鍵字。普通的函數僅僅經過 return 返回結果,而 generator 函數在 yield 時返回結果。npm

咱們能夠讀出上面函數的意圖 「每次你請求下一個值,它都會給你一個從 0 到 max 的值,直到程序退出(直到人類科技毀滅)。」

根據上面的解讀,咱們僅僅在須要時纔會獲得一個值,這是很是重要的,不然,無限序列會很快的耗盡咱們的內存。咱們使用迭代器來獲取須要的值:

var iterator = generateRandoms();

console.log(iterator.next());     // { value: 0.4900301224552095, done: false }
console.log(iterator.next());     // { value: 0.8244022422935814, done: false }

Generators 容許兩種交互,正如咱們下面將要看到的,generators 在沒有被調用時會被掛起,而當迭代器請求下一個值時會被喚醒。因此當咱們屌用 iterator.next 而且傳遞了參數後,參數會被賦值到 newMax:

console.log(iterator.next());     // { value: 0.4900301224552095, done: false }

// 爲 `newMax` 賦值,該值會一直存在
console.log(iterator.next(1000)); // { value: 963.7744706124067, done: false }
console.log(iterator.next());     // { value: 714.516609441489, done: false }

在 ES5 中使用 Generators

爲了更好的理解 generators 工做原理,咱們能夠看一下 generators 是怎樣轉換成 ES5 代碼的。你能夠安裝 babel 而後看一下它轉換後的代碼:

npm install -g babel

babel generate-randoms.js

下面是轉換後的代碼:

var generateRandoms = regeneratorRuntime.mark(function generateRandoms(max) {
  var newMax;
  return regeneratorRuntime.wrap(function generateRandoms$(context$1$0) {
    while (1) switch (context$1$0.prev = context$1$0.next) {
      case 0:
        max = max || 1;

      case 1:
        if (!true) {
          context$1$0.next = 8;
          break;
        }
        context$1$0.next = 4;
        return Math.random() * max;
      case 4:
        newMax = context$1$0.sent;
        if (newMax !== undefined) {
          max = newMax;
        }
        context$1$0.next = 1;
        break;
      case 8:
      case "end":
        return context$1$0.stop();
    }
  }, generateRandoms, this);
});

如你所見,generator 函數的核心代碼被轉換成了 switch 塊, 這對於咱們探索其內部原理提供了頗有價值的線索。咱們能夠把 generator 想象成一個循環狀態機,它根據咱們的交互切換不一樣的狀態。變量 context$1$0 保存了當前的狀態,case 語句都在該狀態中之行。

看一下這些 switch 塊的條件:

  • case 0: 初始化 max 的值而且執行到了 case1

  • case 1: 返回一個隨機值而後 GOTO 4

  • case 4: 檢查迭代器是否設置了 newMax 的值,若是是,就更新 max 的值,而後 GOTO 1, 返回一個隨機值。

這就解釋了爲何 generator 能夠在遵循非阻塞的原則上能夠無限循環和暫停。

什麼時候退出循環

讀者可能注意到我跳過了 9-12 行的代碼:

if (!true) {
    context$1$0.next = 8;
    break;
}

這裏發生了什麼?它實際上是原始代碼 while (true) 被轉換後的代碼。每當狀態機循環時,它都會檢查是否已經到了最後一步。在咱們的示例中,是沒有循環結束的,但你在編碼時可能會遇到不少時候須要退出循環。當符合循環結束條件時,狀態機 GOTO 8, generator 之行完畢。

迭代器中的私有狀態

另一個有趣的事情是 generator 是如何爲每個獨立的迭代器保存私有狀態的。由於變量 max 在 regeneratorRuntime.wrap 的外層做用域,它的值會被保留以供以後的 iterator.next() 訪問。
若是咱們調用 randomNumbers() 建立一個新的迭代器,那麼一個新的閉包也會被建立。這也就解釋了迭代器在使用同一個 generator 時有本身的私有狀態而不會相互影響。

狀態機內部

目前爲止,咱們已經看到 switch 的本質就是狀態機。你可能已經注意到這個函數被包了兩層:regeneratorRuntime.markregeneratorRuntime.wrap
這些是 regenerator 模塊,它能夠在 ES5 中定義 ES6 generator 形式的狀態機。

Regenerator runtime 是一個很長的話題,可是咱們會覆蓋一些有趣的部分。首先,咱們能夠看到 generator 從 「Suspended Start」 狀態開始:

function makeInvokeMethod(innerFn, self, context) {
    var state = GenStateSuspendedStart;

    return function invoke(method, arg) {

源代碼: runtime.js:130,133

在這裏並無發生什麼事,它僅僅是建立並返回了一個函數。這也意味着當咱們調用 var iterator = generateRandoms(), generateRandoms 內部並無執行。

當咱們調用 iterator.next(), generator 函數(以前switch 塊中的內容)會在 tryCatch 中被調用:

var record = tryCatch(innerFn, self, context);

源代碼: runtime.js:234

若是返回結果是普通的 return (而不是 throw), 會把結果包裝成 {value, done}。新的狀態是 GenStateCompleted 或者 GenStateSuspendedYield。因爲咱們的示例是無限循環,因此將老是跳轉到掛起狀態。

var record = tryCatch(innerFn, self, context);
        if (record.type === "normal") {
          // If an exception is thrown from innerFn, we leave state ===
          // GenStateExecuting and loop back for another invocation.
          state = context.done
            ? GenStateCompleted
            : GenStateSuspendedYield;

          var info = {
            value: record.arg,
            done: context.done
          };

源代碼: runtime.js:234,245

你能夠用它作什麼?

今天咱們用 generator 函數實現了一個惰性序列狀態機。這個特性如今就可使用: 現代瀏覽器都已經原生支持了 generator, 而對於老的瀏覽器,也很容易作代碼轉換。

一般,作一件事情的方式有多種。從這種層面上講,你可能不須要 generators,但若是它容許咱們以一種更富表現力的方式完成目標,那就是值得的。


做者信息

翻譯自MaxLeap團隊_前端研發人員:Henry Bai
譯者簡介:多年後端及前端開發經驗,現任MaxLeap UX團隊成員,主要從事MaxLeap相關開發,目前對React Native有濃厚興趣。
中文連接:https://blog.maxleap.cn/archi...

相關文章
webpack 入門

做者往期佳做
Redux 最佳實踐「譯」

歡迎訂閱微信公衆號:MaxLeap_yidongyanfa


關於MaxLeap
MaxLeap 移動業務研發的雲服務平臺,爲企業提供包括應用開發所需的後端雲數據庫、雲數據源、雲代碼、雲容器、 IM、移動支付、應用內社交、第三方登陸、社交分享、數據分析、推送營銷,用戶支持等服務, MaxLeap 致力於讓移動應用開發更快速簡單。
官網:https://maxleap.cn/

相關文章
相關標籤/搜索