Generator 是一種非常強力的語法,但它的使用並不廣泛。這篇文章主要介紹如何在JavaScript 中使用Generator,需要的朋友可以參考下
Generator 是一種非常強力的語法,但它的使用並不廣泛(參見下圖twitter 上的調查! )。為什麼這樣呢?相較於async/await,它的使用更複雜,調試起來也不太容易(大多數情況又回到了從前),即使我們可以通過非常簡單的方式獲得類似體驗,但是人們一般會更喜歡async/await 。
然而,Generator 允許我們透過 yield 關鍵字遍歷我們自己的程式碼!這是一種超級強大的語法,實際上,我們可以操縱執行過程!從不太明顯的取消操作開始,讓我們先從同步操作開始。
我為文中提到的功能創建了一個程式碼倉庫- github.com/Bloomca/obs…
批次(或計畫)
執行Generator 函數會傳回一個遍歷器對象,那表示透過它我們可以同步地遍歷。為什麼我們想這麼做?原因有可能是為了實現批次處理。想像一下,我們需要下載 1000 個項目,並在表格中逐行的顯示它們(不要問我為什麼,假設我們不使用框架)。雖然立刻展示它們沒有什麼不好的,但有時這可能不是最好的解決方案 —— 也許你的 MacBook Pro 可以輕鬆處理它,但普通人的電腦不能(更別說手機了)。所以,這意味著我們需要用某種方式延遲執行。
請注意,這個例子是關於效能最佳化,在你遇到這個問題之前,沒必要這樣做——過早優化是萬惡之源!
// 最初的同步实现版本 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 方法完全沒有影響。
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? ) 這篇文章中已經詳細談到了。所以我會使用其中一些程式碼:
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 行數據,而且速度非常慢,我們希望給用戶提供暫停/恢復渲染的功能,這樣他們就可以停止所有的後台工作讀取已經下載的內容了。讓我們開始吧!
// 实现渲染的方法还是一样的 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 某些问题可以被优雅和简洁的处理。
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
以上是在JavaScript中如何使用Generator的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!