- 原文地址:How to Use Generators in JavaScript
- 原文做者:Seva Zaikov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:jonjia
- 校對者:vuuihc congFly
Generator 是一種很是強力的語法,但它的使用並不普遍(參見下圖 twitter 上的調查!)。爲何這樣呢?相比於 async/await,它的使用更復雜,調試起來也不太容易(大多數狀況又回到了從前),即便咱們能夠經過很是簡單的方式得到相似體驗,可是人們通常會更喜歡 async/await。html
然而,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 是如今的首選語法(甚至 co 也談到了它 ),這也是將來。可是,Generator 也在 ECMAScript 標準內,這意味着爲了使用它們,除了寫幾個工具函數,你不須要任何東西。我試圖向大家展現一些不那麼簡單的例子,這些實例的價值取決於你的見解。請記住,沒有那麼多人熟悉 Generator,而且若是在整個代碼庫中只有一個地方使用它們,那麼使用 Promise 可能會更容易一些 —— 可是另外一方面,經過 Generator 某些問題能夠被優雅和簡潔的處理。
明智地選擇 —— 能力越大,責任越重(蜘蛛俠 2,2004)!
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。