[譯] 如何在 JavaScript 中使用 Generator?

如何在 JavaScript 中使用 Generator

Generator 是一種很是強力的語法,但它的使用並不普遍(參見下圖 twitter 上的調查!)。爲何這樣呢?相比於 async/await,它的使用更復雜,調試起來也不太容易(大多數狀況又回到了從前),即便咱們能夠經過很是簡單的方式得到相似體驗,可是人們通常會更喜歡 async/await。html

1513838054(1).jpg

然而,Generator 容許咱們經過 yield 關鍵字遍歷咱們本身的代碼!這是一種超級強大的語法,實際上,咱們能夠操縱執行過程!從不太明顯的取消操做開始,讓咱們先從同步操做開始吧。前端

我爲文中提到的功能建立了一個代碼倉庫 —— github.com/Bloomca/obs…android

批處理 (或計劃)

執行 Generator 函數會返回一個遍歷器對象,那意味着經過它咱們能夠同步地遍歷。爲何咱們想這麼作?緣由有多是爲了實現批處理。想象一下,咱們須要下載 1000 個項目,並在表格中逐行的顯示它們(不要問我爲何,假設咱們不使用框架)。雖然馬上展現它們沒有什麼很差的,但有時這可能不是最好的解決方案 —— 也許你的 MacBook Pro 能夠輕鬆處理它,但普通人的電腦不能(更別說手機了)。因此,這意味着咱們須要用某種方式延遲執行。ios

請注意,這個例子是關於性能優化,在你遇到這個問題以前,不必這樣作 —— 過早優化是萬惡之源!git

// 最初的同步實現版本
function renderItems(items) {
  for (item of items) {
    renderItem(item);
  }
}

// 函數將由咱們的執行器遍歷執行
// 實際上,咱們能夠用相同的同步方式來執行它!
function* renderItems(items) {
  // 我使用 for..of 遍歷方法來避免新函數的產生
  for (item of items) {
    yield renderItem(item);
  }
}
複製代碼

沒有什麼區別是吧?那麼,這裏的區別在於,如今咱們能夠在不改變源代碼的狀況下以不一樣方式運行這個函數。實際上,正如我以前提到的,沒有必要等待,咱們能夠同步執行它。因此,來調整下咱們的代碼。在每一個 yield 後邊加一個 4 ms(JavaScript VM 中的一個心跳) 的延遲怎麼樣?咱們有 1000 個項目,渲染將須要 4 秒 —— 還不錯,假設我想在 2 秒以內渲染完畢,很容易想到的方法是每次渲染 2 個。忽然使用 Promise 的解決方案將變得更加複雜 —— 咱們必需要傳遞另外一個參數:每次渲染的項目個數。經過咱們的執行器,咱們仍然須要傳遞這個參數,但好處是對咱們的 renderItems 方法徹底沒有影響。github

function runWithBatch(chunk, fn, ...args) {
  const gen = fn(...args);
  let num = 0;
  return new Promise((resolve, promiseReject) => {
    callNextStep();

    function callNextStep(res) {
      let result;
      try {
        result = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        return resolve(value);
      }

      // every chunk we sleep for a tick
      if (num++ % chunk === 0) {
        return sleep(4).then(proceed);
      } else {
        return proceed();
      }

      function proceed() {
        return callNextStep(value);
      }
    }
  });
}

// 第一個參數 —— 每批處理多少個項目
const items = [...];
batchRunner(2, function*() {
  for (item of items) {
    yield renderItem(item);
  }
});
複製代碼

正如你所看到的,咱們能夠輕鬆改變每批處理項目的個數,不去考慮執行器,回到正常的同步執行方式 —— 全部這些都不會影響咱們的 renderItems 方法。後端

取消

咱們來考慮下傳統的功能 —— 取消。在我 promises cancellation in general (譯文:如何取消你的 Promise?) 這篇文章中已經詳細談到了。因此我會使用其中一些代碼:promise

function runWithCancel(fn, ...args) {
  const gen = fn(...args);
  let cancelled, cancel;
  const promise = new Promise((resolve, promiseReject) => {
    // define cancel function to return it from our fn
    // 定義 cancel 方法,並返回它
    cancel = () => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    };

    onFulfilled();

    function onFulfilled(res) {
      if (!cancelled) {
        let result;
        try {
          result = gen.next(res);
        } catch (e) {
          return reject(e);
        }
        next(result);
        return null;
      }
    }

    function onRejected(err) {
      var result;
      try {
        result = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        return resolve(value);
      }
      // 假設咱們老是接收 Promise,因此不須要檢查類型
      return value.then(onFulfilled, onRejected);
    }
  });

  return { promise, cancel };
}
複製代碼

這裏最好的部分是咱們能夠取消全部還沒來得及執行的請求(也能夠給咱們的執行器傳遞相似 AbortController 的對象參數,因此它甚至能夠取消當前的請求!),並且咱們沒有修改過本身業務邏輯中的一行的代碼。性能優化

暫停/恢復

另外一個特殊的需求多是暫停/恢復功能。你爲何想要這個功能?想象一下,咱們渲染了 1000 行數據,並且速度很是慢,咱們但願給用戶提供暫停/恢復渲染的功能,這樣他們就能夠中止全部的後臺工做讀取已經下載的內容了。讓咱們開始吧!bash

// 實現渲染的方法仍是同樣的
function* renderItems() {
  for (item of items) {
    yield renderItem(item);
  }
}

function runWithPause(genFn, ...args) {
  let pausePromiseResolve = null;
  let pausePromise;

  const gen = genFn(...args);

  const promise = new Promise((resolve, reject) => {
    onFulfilledWithPromise();

    function onFulfilledWithPromise(res) {
      if (pausePromise) {
        pausePromise.then(() => onFulfilled(res));
      } else {
        onFulfilled(res);
      }
    }

    function onFulfilled(res) {
      let result;
      try {
        result = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(result);
      return null;
    }

    function onRejected(err) {
      var result;
      try {
        result = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        return resolve(value);
      }
      // 假設咱們老是接收 Promise,因此不須要檢查類型
      return value.then(onFulfilledWithPromise, onRejected);
    }
  });

  return {
    pause: () => {
      pausePromise = new Promise(resolve => {
        pausePromiseResolve = resolve;
      });
    },
    resume: () => {
      pausePromiseResolve();
      pausePromise = null;
    },
    promise
  };
}
複製代碼

調用這個執行器,能夠給咱們返回一個具備暫停/恢復功能的對象,全部這些均可以輕鬆獲得,仍是使用咱們以前的業務代碼!因此,若是你有不少"沉重"的請求鏈,須要耗費很長時間,而你想給你的用戶提供暫停/恢復功能的話,你能夠隨意在你的代碼中實現這個執行器。

錯誤處理

咱們有個神祕的 onRejected 調用,這是咱們這部分談論的主題。若是咱們使用正常的 async/await 或 Promise 鏈式寫法,咱們將經過 try/catch 語句來進行錯誤處理,若是不添加大量的邏輯代碼就很難進行錯誤處理。一般狀況下,若是咱們須要以某種方式處理錯誤(好比重試),咱們只是在 Promise 內部進行處理,這將會回調本身,可能再次回到一樣的點。並且,這還不是一個通用的解決方案 —— 可悲的是,在這裏甚至 Generator 也不能幫助咱們。咱們發現了 Generator 的侷限 —— 雖然咱們能夠控制執行流程,但不能移動 Generator 函數的主體;因此咱們不能後退一步,從新執行咱們的命令。一個可行的解決方案是使用 command pattern, 它告訴了咱們 yield 結果的數據結構 —— 應該是咱們須要執行此命令須要的全部信息,這樣咱們就能夠再次執行它了。因此,咱們的方法須要改成:

function* renderItems() {
  for (item of items) {
    // 咱們須要將全部東西傳遞出去:
    // 方法, 內容, 參數
    yield [renderItem, null, item];
  }
}

複製代碼

正如你所看到的,這使得咱們不清楚發生了什麼 —— 因此,也許最好是寫一些 wrapWithRetry 方法,它會檢查 catch 代碼塊中的錯誤類型並再次嘗試。可是咱們仍然能夠作一些不影響咱們功能的事情。例如,咱們能夠增長一個關於忽略錯誤的策略 —— 在 async/await 中咱們不得不使用 try/catch 包裝每一個調用,或者添加空的 .catch(() => {}) 部分。有了 Generator,咱們能夠寫一個執行器,忽略全部的錯誤。

function runWithIgnore(fn, ...args) {
  const gen = fn(...args);
  return new Promise((resolve, promiseReject) => {
    onFulfilled();

    function onFulfilled(res) {
      proceed({ data: res });
    }

    // 這些是 yield 返回的錯誤
    // 咱們想忽略它們
    // 因此咱們像往常同樣作,但不去傳遞出錯誤
    function onRejected(error) {
      proceed({ error });
    }

    function proceed(data) {
      let result;
      try {
        result = gen.next(data);
      } catch (e) {
        // 這些錯誤是同步錯誤(好比 TypeError 等)
        return reject(e);
      }
      // 爲了區分錯誤和正常的結果
      // 咱們用它來執行
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        return resolve(value);
      }
      // 假設咱們老是接收 Promise,因此不須要檢查類型
      return value.then(onFulfilled, onRejected);
    }
  });
}
複製代碼

關於 async/await

Async/await 是如今的首選語法(甚至 co 也談到了它 ),這也是將來。可是,Generator 也在 ECMAScript 標準內,這意味着爲了使用它們,除了寫幾個工具函數,你不須要任何東西。我試圖向大家展現一些不那麼簡單的例子,這些實例的價值取決於你的見解。請記住,沒有那麼多人熟悉 Generator,而且若是在整個代碼庫中只有一個地方使用它們,那麼使用 Promise 可能會更容易一些 —— 可是另外一方面,經過 Generator 某些問題能夠被優雅和簡潔的處理。

明智地選擇 —— 能力越大,責任越重(蜘蛛俠 2,2004)!

相關文章


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄