この記事では主に、JavaScript でのジェネレーターの使用方法について説明します。ジェネレーターは非常に強力な構文ですが、その使用法はあまり広くありません (以下の Twitter のアンケートを参照してください)。なぜそうなるのでしょうか? async/await と比較すると、その使用はより複雑で、デバッグは簡単ではありません (ほとんどの場合、非常に簡単な方法で同様のエクスペリエンスを得ることができますが、一般的には async/await が好まれます。
ただし、ジェネレーターを使用すると、yield キーワードを使用して独自のコードを反復処理できます。これは非常に強力な構文であり、実際に実行を操作できます。あまり目立たないキャンセル操作から始めて、同期操作を始めましょう。
記事で言及されている関数のコード リポジトリを作成しました - github.com/Bloomca/obs…
バッチ処理 (または計画)
Generator 関数を実行するとトラバーサー オブジェクトが返されます。これは、それを通じて次のことができることを意味します同期的に走査されます。なぜこれを行う必要があるのでしょうか?バッチ処理を実装するためである可能性があります。 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 内で処理を行うだけで、Promise 自体がコールバックされ、場合によっては再び同じポイントに戻ります。そして、これはまだ普遍的な解決策ではありません。悲しいことに、Generator ですらここでは役に立ちません。ジェネレーターの制限が見つかりました。実行フローを制御することはできますが、ジェネレーター関数の本体を移動することはできないため、コマンドを後退して再実行することはできません。考えられる解決策は、コマンド パターンを使用することです。コマンド パターンは、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 某些问题可以被优雅和简洁的处理。
相关推荐:
Promise,Generator(生成器),async(异步)函数的用法
以上がJavaScript でジェネレーターを使用する方法の詳細な例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。