ホームページ > 記事 > ウェブフロントエンド > JavaScript でジェネレーター メソッドを使用する方法
ジェネレーターは非常に強力な構文ですが、あまり広く使用されていません。この記事では主に JavaScript でのジェネレーターの使用方法を紹介します。必要な方は参考にしてください。
ジェネレーターは非常に強力な構文ですが、あまり広く使用されていません (以下の twitter のアンケートを参照してください)。なぜそうなるのでしょうか? async/await と比較すると、その使用はより複雑で、デバッグは簡単ではありません (ほとんどの場合、非常に簡単な方法で同様のエクスペリエンスを得ることができますが、一般的には async/await が好まれます。
ただし、ジェネレーターを使用すると、yield キーワードを使用して独自のコードを反復処理できます。これは非常に強力な構文であり、実際に実行を操作できます。あまり目立たないキャンセル操作から始めて、同期操作を始めましょう。
記事で言及されている関数のコード リポジトリを作成しました - github.com/Bloomca/obs…
バッチ (または計画)
Generator 関数を実行するとトラバーサー オブジェクトが返されます。つまり、Through it同期的にトラバースできます。なぜこれを行う必要があるのでしょうか?バッチ処理を実装するためである可能性があります。 1000 個の項目をダウンロードし、それらを表に 1 行ずつ表示する必要があると想像してください (フレームワークを使用しないとして、理由は聞かないでください)。すぐに見せびらかすのは悪いことではありませんが、場合によってはそれが最善の解決策ではない可能性があります。おそらくあなたの MacBook Pro なら簡単に処理できるかもしれませんが、平均的な人のコンピュータでは (携帯電話はもちろんのこと) 処理できません。つまり、何らかの方法で実行を遅らせる必要があるということになります。
この例はパフォーマンスの最適化に関するものであることに注意してください。この問題が発生するまでこれを行う必要はありません。時期尚早な最適化は諸悪の根源です。
// 最初的同步实现版本 function renderItems(items) { for (item of items) { renderItem(item); } } // 函数将由我们的执行器遍历执行 // 实际上,我们可以用相同的同步方式来执行它! function* renderItems(items) { // 我使用 for..of 遍历方法来避免新函数的产生 for (item of items) { yield renderItem(item); } }
違いはありませんね?ここでの違いは、ソース コードを変更せずにこの関数を別の方法で実行できることです。実際には、前に述べたように、待つ必要はなく、同期的に実行できます。それでは、コードを微調整してみましょう。各利回りの後に 4 ミリ秒 (JavaScript VM の 1 ハートビート) の遅延を追加してはどうでしょうか?アイテムが 1000 個あり、レンダリングには 4 秒かかります。2 秒でレンダリングしたいと仮定すると、悪くはありません。簡単に考えるのは、一度に 2 つをレンダリングすることです。 Promises を使用したソリューションは突然、より複雑になります。別のパラメーター、つまり毎回レンダリングする項目の数を渡す必要があります。エグゼキュータを介してこのパラメータを渡す必要がありますが、利点は、これが 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 メソッドに影響を与えません。
キャンセル
従来の機能であるキャンセルについて考えてみましょう。これについては、私の記事「約束のキャンセル一般」(翻訳:約束をキャンセルするにはどうすればいいですか?)で詳しく説明しました。そこで、このコードの一部を使用します:
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 のようなオブジェクト パラメータをエグゼキュータに渡すこともできるので、現在のリクエストをキャンセルすることもできます)、ビジネス ロジックのコードは 1 行も変更されていません。
一時停止/再開
もう 1 つの特別なニーズは、一時停止/再開機能です。この機能が必要な理由は何ですか? 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 でジェネレーター メソッドを使用する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。