このテーマの共有は、阮一峰先生の ECMAScript6 入門を参照しています => https://es6.ruanyifeng.com/#docs/generator#%E7%AE%80%E4%BB%8B
参照リスト:
直感的な使用#
function* printStrGenerator() {
let str1 = yield "こんにちは";
console.log(str1);
let str2 = yield str1 + "CodeGorgeous";
console.log(str2);
return "完了";
}
const print = printStrGenerator();
console.log(print); // printStrGenerator {<suspended>}
console.log(print.next()); // {done: false, value: "こんにちは"}
console.log(print.next("私の名前は")); // {done: false, value: "CodeGorgeous"}
console.log(print.next("テスト")); // {done: true, value: "完了"}
console.log(print.next()); // {done: true, value: undefined}
console.log(print); // printStrGenerator {<closed>}
Generator の背景#
JavaScript はシングルスレッドで動作するため、リクエストを発行する際にレスポンスを待ってからコードを実行すると非常に不安定な時間を浪費することになります。この問題を解決するために、非同期コードプログラミングという方法が導入されました。これは、コードが非同期操作に到達したときに後続のコードを実行し、非同期操作が完了した後に適切な操作をトリガーし、同期コードを実行するという考え方です。この考え方に基づいて、いくつかの解決策が提案されました:
- コールバック関数
- イベントリスナー
- 発行 / 購読
- Promise
いくつかの例を通じて、これらの解決策とその利点・欠点を再確認しましょう
-
コールバック関数
- 例:
// ファイルコピー操作 fs.readFile("./test/index.html", 'utf-8', (err, data) => { if (err) return; // ファイルを作成し、内容を書き込む fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => { if (err) return; console.log("コピー成功"); }) });
- 利点:
- コードの実行をブロックしない、書きやすい
- 欠点:
- 一つの非同期操作が前の非同期操作に依存し、コールバック地獄 (callback hell) を形成しやすい(高い結合度)
- コードの保守が難しい
-
イベントリスナー
- 例:
const fn = () => { fs.readFile("./test/index.html", 'utf-8', (err, data) => { if (err) return; fn.trigger("readFileOver", data); }); }; fn.eventList = []; fn.on = (type, callback) => { fn.eventList.push({ type, fn: callback }); }; fn.trigger = (type, ...args) => { fn.eventList.forEach(item => { (item.type === type) && (item.fn(...args)); }); }; fn.on("readFileOver", (data) => { fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => { if (err) return; console.log("コピー成功"); }); }); fn();
- 利点:
- コールバック関数方式に比べてコールバック地獄をほとんど形成しない(結合度が低い)
- 複数のイベントを自分でバインドでき、コールバック関数を個別に設定・調整できる
- 例:
-
発行 / 購読(オブザーバーパターン)
- 例:
class EventBus { constructor() { this.event = []; }; /** * 購読 * @param {*} eventType イベントタイプ * @param {*} callback コールバック関数 */ add(eventType, callback) { this.event.push({ type: eventType, fn: callback }); }; /** * 発行 * @param {*} eventType イベントタイプ * @param {...any} args 引数 */ trigger(eventType, ...args) { // ここでは発火したイベントの削除操作は行わない this.event.forEach(item => { if (item.type === eventType) { item.fn(...args); } }); } }; // インスタンスを作成 const eventBus = new EventBus(); // readFileOverイベントを購読 eventBus.add("readFileOver", (data) => { fs.writeFile("./test/index-copy1.html", data, 'utf-8', (err) => { if (err) return; console.log("コピー成功1"); }); }); eventBus.add("readFileOver", (data) => { fs.writeFile("./test/index-copy2.html", data, 'utf-8', (err) => { if (err) return; console.log("コピー成功2"); }); }); fs.readFile("./test/index.html", 'utf-8', (err, data) => { if (err) return; // readFileOverイベントを発行 eventBus.trigger("readFileOver", data); });
- 利点:
- イベントリスナーに比べて発行 / 購読モデルはトリガー機構の管理が容易で、現在存在するイベントの状況を明確に把握できる
- 例:
-
Promise
- 例:
new Promise((resolve, reject) => { fs.readFile("./test/index.html", 'utf-8', (err, data) => { if (err) reject(err); resolve(data); }); }).then(resp => { // ファイルを作成し、内容を書き込む fs.writeFile("./test/index-copy.html", resp, 'utf-8', (err) => { if (err) return; console.log("コピー成功"); }); }).catch(err => { console.log("エラー:", err); });
- 利点:
- 従来のコールバック関数の内蔵モードからチェーン呼び出しモードに変更
- より明確なコードフロー
- 可読性の向上
- 例:
従来のプログラミング言語にはすでに非同期プログラミングの解決策が存在しており、その中にはコルーチンと呼ばれる解決策もあります。これは、スレッドが相互に協力して非同期タスクを完了することを意味します。
JavaScript では Generator 関数の形で登場し、ファイルコピーの例を挙げると:
function* copyFile() {
const data = yield fs.readFile("./test/index.html", 'utf-8', (err, data) => {
if (err) return;
else fn.next(data);
});
yield fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
if (err) return;
console.log("コピー成功");
fn.next();
})
};
const fn = copyFile();
fn.next()
なぜ Generator は非同期問題を解決できるのか
Generator の最も重要な特徴は、関数の実行権を譲渡できることです。実行権は、関数内のコードが実行できるかどうか、または関数内のコードの実行を一時停止する権利を表します。
実行権を手動で操作することで、適切なタイミング(例えば、リクエストのレスポンスを受け取ったとき)で関数内のコードを続行できます。
Generator の使用#
Generator 関数を作成する方法
Generator と通常の関数の最大の違いは、Generator 関数を宣言する際に * 記号を付ける必要があることです。例えば:
function* generator() {}
function * generator() {}
function*generator() {}
function *generator() {}
これらの 4 つはすべて Generator 関数の書き方です(優劣はありません)。一般的には最初の書き方が推奨され、多くのコードフォーマッタは Generator を最初の書き方にフォーマットします。また、Generator 関数はコンストラクタとして使用できないことにも注意が必要です。
Generator 関数の実行特性
Generator 関数を実行すると、関数内に書いたものは何も実行されていないことに気づくでしょう。これは、関数の実行権を管理できることの一つの表れです。関数の戻り値を受け取ると、デフォルトでは一時停止状態の generator オブジェクトが返されます。このオブジェクトの next () を使用して、関数内のコードの実行権を一度実行することができます。この実行の範囲は、初回実行の場合は関数の開始から次の yield 式の位置 /return 位置まで、その他の場合は前回の yield 式の位置から次の yield 式の位置 /return 位置までです。next () メソッドを実行するたびに、次のようなオブジェクトが得られます: {value: undefined, done: false}
, このオブジェクトの value は yield 式 /return 式の戻り値を表し、done はその Generator が最後の関数内コードの実行であるかどうかを示します。true はその後 next () を使用しても意味がないことを示し、false はそうではありません。
Generator 実行の示意図
yield 式#
上記の Generator の紹介と特徴から、Generator における yield 式の重要性が理解できます。yield は Generator 内でのコード実行範囲(つまり、前の yield から今回の yield までの間のコード)を表します。同様に、yield は他の関数内で使用できるのでしょうか?明らかにできません。なぜなら、他の関数内では存在しない意味がないからです。なぜなら、他の関数は関数内部の実行権を制御できないからです。
最も注意すべき点は、yield の戻り値は変数に代入されないことです。yield の値は next () 関数の実行の戻り値として渡されます。next 関数を呼び出すときに渡す引数は、前回の yield の実行結果として代入操作に現れます。このように言葉で説明すると混乱しますので、以下の図で説明します:
Generator と Iterator#
Iterator を理解する
Iterator は ES6 で導入された新しい反復メカニズムで、以下の特徴があります:
- Symbol.iterator インターフェース属性を持つ
- forof で反復可能(forof は Iterator インターフェースを持つデータのみを反復可能)
- Symbol.iterator 属性が返す関数は Generator 特性を持ち、実行権を制御する生成器です
Generator は反復器の生成器であるため、Symbol.iterator 属性を持たないオブジェクトに手動で反復器インターフェースを追加して、そのオブジェクトが forof 反復やいくつかのメソッドを使用できるようにすることができます。興味があれば、以下のコードを見てみてください:
const obj = {};
obj[Symbol.iterator] = function* () {
yield "CodeGorgeous";
yield "MaoMao";
yield "BestFriend";
};
for (const item of arr) {
console.log(item);// CodeGorgeous // MaoMao // BestFriend
};
const arrObj = [...obj];
console.log(arrObj);// [ 'CodeGorgeous', 'MaoMao', 'BestFriend' ]
Generator と throw#
通常の JavaScript コードでは、trycatch 式を使用してコードの一部を包み、エラーが発生した場合に他のコードの実行に影響を与えないようにします。もしあなたが第三者のコードを見たことがあるなら、関数内で渡された引数が期待される型に合致するかどうかを最初に判定し、合致しない場合は手動でthrow
を使用してエラーを投げることがあります。Generator では、生成器内でエラーを投げるための throw メソッドが提供されています。
function* printStrGenerator() {
let index = 0;
while (true) {
try {
yield `これは${index ++}です`
} catch (error) {
console.log("Generator内部でエラーをキャッチしました", error);
}
}
};
const g = printStrGenerator();
console.log(g.next()); // {value: 'これは0です', done: false}
try {
g.throw(new Error("コード内で予期されたエラーが発生しました")); // Generator内部でエラーをキャッチしました Error: コード内で予期されたエラーが発生しました
g.throw(new Error("コード内で予期されたエラーが発生しました")); // Generator内部でエラーをキャッチしました Error: コード内で予期されたエラーが発生しました
} catch (error) {
console.log("ウィンドウ外でエラーをキャッチしました");
}
console.log(g.next()); // {value: 'これは3です', done: false}
この例からわかるように、Generator.prototype.throw はエラーを投げる機能だけでなく、次の next () を実行する特性も持っています。この例では無限ループと trycatch を使用しているため、エラーを投げるたびに Generator 内部でエラーをキャッチします(ただし、各部分は異なるブロックスコープであり、同じスコープではないことを明確に理解する必要があります)。もしあなたが各 yield 式を関数内部の trycatch でキャッチしていなかったり、現在の Generator がすでに閉じているのにまだ throw を投げると、外部の trycatch によってキャッチされます(内部キャッチと外部キャッチは完全に異なる意味を持ちます)。さらに注意すべき点は、Generator 関数の実行中に内部でエラーが発生したが内部でキャッチされなかった場合(ps: 外部でキャッチする必要があります。外部でキャッチしなければプログラムはクラッシュします)、Generator の実行が終了し、その後 next を続けても{value: undefined, done: true}
が返されます。言葉で説明するのが難しいので、対照グループを作成して違いを明確に観察しましょう:
- Generator 内部でキャッチされず、外部でキャッチされる
function* printStrGenerator() {
let index = 0;
while (true) {
yield `これは${index ++}です`;
}
};
const g = printStrGenerator();
try {
console.log(g.next()); // {value: 'これは0です', done: false}
console.log(g.throw(new Error("これはエラーです"))); // このステップでGeneratorはエラーを報告します
} catch (error) {
console.log(g.next()); // {value: undefined, done: true}
console.log(g); // printStrGenerator {<closed>}
}
- Generator 内部でキャッチされ、外部でキャッチされない
function* printStrGenerator() {
let index = 0;
while (true) {
try {
yield `これは${index ++}です`;
} catch (error) {
console.log(error);
}
}
};
const g = printStrGenerator();
console.log(g.next()); // {value: 'これは0です', done: false}
console.log(g.throw(new Error("これはエラーです"))); // エラー: これはエラーです // {value: 'これは1です', done: false}
console.log(g.next()); // {value: 'これは2です', done: false}
console.log(g); // printStrGenerator {<suspended>}
Generator と return#
通常、return は関数の実行を終了し、デフォルトで undefined を返す場合に出現します。ここでは、Generator.prototype.return メソッドについて議論します。なぜこの 2 つの異なるレベルのものを言及するのかというと(ps: 一つは文法レベル、もう一つは関数です)、Generator の原型チェーン(原型チェーンに不慣れな方は追加の知識部分を見てください)の return も同様の効果を持つことができます。以下の例を観察することで効果を確認できます:
function* printStrGenerator() {
let index = 0;
while (true) {
yield `これは${index ++}です`;
}
};
const g = printStrGenerator();
console.log(g.next()); // {value: 'これは0です', done: false}
console.log(g.return("完了")); // {value: '完了', done: true}
console.log(g.next()); // {value: undefined, done: true}
ネストされた Generator の処理方法#
異なる非同期を異なる generator に分離して抽象化し、使用時に全体の Generator を組み合わせて管理するというシナリオを考えてみましょう。このアイデアは確かに素晴らしいです。一方で、抽象化によって再利用性が向上し、他方で論理の分離が容易になります(この関数を使用する際にその内部のことを考える必要がありません)。さらに、メンテナンス性が高く、特定の Generator に問題が発生した場合、通常はその関数を修正するだけで済み、他のコードを調整する必要はありません。それでは、実際に試してみましょう。
function* getUserInfoGenerator(generator) {
return yield (setTimeout(() => {
const useInfo = {
nickname: "CodeGorgeous",
openid: "aszxzxcISadaxzxxxxxx"
};
console.log("ユーザー情報の取得に成功しました");
generator && generator(useInfo);
}, Math.random() * 5000));
}
function* getProjectConfigGenerator(generator) {
return yield (setTimeout(() => {
const projectConfig = {
theme: "default",
lang: "zh-CN",
title: "集中一点, 登峰造极⚡"
};
console.log("プロジェクト設定の取得に成功しました");
generator && generator(projectConfig);
}, Math.random() * 5000))
}
function* dispatchGenerator() {
yield "1";
console.log("ユーザー情報の取得を開始します");
const useInfo = yield getUserInfoGenerator;
yield "2";
console.log(`取得したプロジェクト設定`, useInfo);
// console.log(`取得した${useInfo && useInfo.nickname}ユーザーのプロジェクト設定`, useInfo);
const projectConfig = yield getProjectConfigGenerator;
console.log(projectConfig);
}
// 実行調整
const g = dispatchGenerator();
// ここでは純粋に突発的なアイデアでGenerator自動実行器を作成します(これを書くのは本当に大変です、また髪が抜け始めました)
function run(generator) {
function next(data) {
var result = generator.next(data);
if (result.done) return;
if (typeof result.value === 'function' && result.value[Symbol.toStringTag] === "GeneratorFunction") {
const temp = result.value(next);
run(temp);
} else {
next();
}
}
next();
};
run(g);
しかし、Generator では yield 式がネストされた Generator をフラット化する別の方法を提供します。この方法は、内蔵 Generator に遭遇した場合にyield*を使用することです。上記のコードを少し改造してみましょう。
function* getUserInfoGenerator(generator) {
return yield (setTimeout(() => {
const useInfo = {
nickname: "CodeGorgeous",
openid: "aszxzxcISadaxzxxxxxx"
};
console.log("ユーザー情報の取得に成功しました");
generator && generator.next(useInfo);
}, Math.random() * 5000));
}
function* getProjectConfigGenerator(generator) {
return yield (setTimeout(() => {
const projectConfig = {
theme: "default",
lang: "zh-CN",
title: "集中一点, 登峰造极⚡"
};
console.log("プロジェクト設定の取得に成功しました");
generator && generator.next(projectConfig);
}, Math.random() * 5000))
}
function* dispatchGenerator() {
console.log("ユーザー情報の取得を開始します");
const useInfo = yield* getUserInfoGenerator(g);
console.log(`取得した${useInfo.nickname}ユーザーのプロジェクト設定`, useInfo);
const projectConfig = yield* getProjectConfigGenerator(g);
console.log(projectConfig);
}
// 実行調整
const g = dispatchGenerator();
g.next();
注意すべき点は、上記の 2 つの例で非同期操作の処理が不合理であることです。機能を実現できるものの、実際に非同期操作に適用する場合はGenerator 非同期シーンの応用を見て、そこで合理的なコードを書き直します。
Generator とコルーチンについての考察#
この部分は別の記事で記録するつもりです。なぜなら、考察していると話が逸れやすいからです。
Generator 非同期シーンの応用#
Generator 自体は非同期シーンを処理できません。その主な機能は非同期操作のコンテナとして登場することです。日常の開発では通常 Generator を使用しませんが、私が普段遭遇するライブラリ(redux-saga)は基本的に Generator に基づいて開発されています。興味があれば React やそのエコシステムを調べてみてください。それでは、日常の開発でどのように解決できるかというと、最初に述べた非同期解決策を使用します。以下に 2 つの方法を示します:
- 解決策:
- コールバック関数
- コールバック関数のタイミングで関数の実行権を適時返す
function getUserInfoGenerator(callback) { return (setTimeout(() => { const useInfo = { nickname: "CodeGorgeous", openid: "aszxzxcISadaxzxxxxxx" }; console.log("ユーザー情報の取得に成功しました"); callback(useInfo); }, Math.random() * 5000)) } function getProjectConfigGenerator(callback) { return (setTimeout(() => { const projectConfig = { theme: "default", lang: "zh-CN", title: "集中一点, 登峰造极⚡" }; console.log("プロジェクト設定の取得に成功しました"); callback(projectConfig); }, Math.random() * 5000)) } function* dispatchGenerator() { console.log("ユーザー情報の取得を開始します"); const useInfo = yield getUserInfoGenerator; console.log(`取得した${useInfo && useInfo.nickname}ユーザーのプロジェクト設定`, useInfo); const projectConfig = yield getProjectConfigGenerator; console.log(projectConfig); } // 実行調整 const g = dispatchGenerator(); function run(generator) { function next(data) { var result = generator.next(data); if (result.done) return; if (typeof result.value === 'function') { result.value((data) => { next(data); }); } else { next(result.value); } } next(); }; run(g);
- Promise
- then のタイミングで関数の実行権を適時返す
function getUserInfoGenerator() { return new Promise(resolve => { setTimeout(() => { const useInfo = { nickname: "CodeGorgeous", openid: "aszxzxcISadaxzxxxxxx" }; console.log("ユーザー情報の取得に成功しました"); resolve(useInfo); }, Math.random() * 5000) }); } function getProjectConfigGenerator() { return new Promise(resolve => { setTimeout(() => { const projectConfig = { theme: "default", lang: "zh-CN", title: "集中一点, 登峰造极⚡" }; console.log("プロジェクト設定の取得に成功しました"); resolve(projectConfig); }, Math.random() * 5000) }) } function* dispatchGenerator() { console.log("ユーザー情報の取得を開始します"); const useInfo = yield getUserInfoGenerator(); console.log(`取得した${useInfo && useInfo.nickname}ユーザーのプロジェクト設定`, useInfo); const projectConfig = yield getProjectConfigGenerator(); console.log(projectConfig); } // 実行調整 const g = dispatchGenerator(); function run(generator) { function next(data) { var result = generator.next(data); if (result.done) return; if (typeof result.value === 'object' && result.value.constructor.name === "Promise") { result.value.then(data => { next(data); }); } else { next(result.value); } } next(); }; run(g);
- コールバック関数
Generator と async#
async は ES2017 バージョンで導入された関数で、async の登場により非同期操作の管理がより便利で簡潔になります。なぜここで Generator から async に話が移るのかというと、以下の 2 つの方法を観察してみましょう:
- Generator 方式
function getUserInfoGenerator(callback) {
return (setTimeout(() => {
const useInfo = {
nickname: "CodeGorgeous",
openid: "aszxzxcISadaxzxxxxxx"
};
console.log("ユーザー情報の取得に成功しました");
callback(useInfo);
}, Math.random() * 5000))
}
function getProjectConfigGenerator(callback) {
return (setTimeout(() => {
const projectConfig = {
theme: "default",
lang: "zh-CN",
title: "集中一点, 登峰造极⚡"
};
console.log("プロジェクト設定の取得に成功しました");
callback(projectConfig);
}, Math.random() * 5000))
}
function* dispatchGenerator() {
console.log("ユーザー情報の取得を開始します");
const useInfo = yield getUserInfoGenerator;
console.log(`取得した${useInfo && useInfo.nickname}ユーザーのプロジェクト設定`, useInfo);
const projectConfig = yield getProjectConfigGenerator;
console.log(projectConfig);
}
// 実行調整
const g = dispatchGenerator();
function run(generator) {
function next(data) {
var result = generator.next(data);
if (result.done) return;
if (typeof result.value === 'object' && result.value.constructor.name === "Promise") {
result.value.then(data => {
next(data);
});
} else {
next(result.value);
}
}
next();
};
run(g);
- async 方式
function getUserInfoGenerator() {
return new Promise(resolve => {
setTimeout(() => {
const useInfo = {
nickname: "CodeGorgeous",
openid: "aszxzxcISadaxzxxxxxx"
};
console.log("ユーザー情報の取得に成功しました");
resolve(useInfo);
}, Math.random() * 5000)
});
}
function getProjectConfigGenerator() {
return new Promise(resolve => {
setTimeout(() => {
const projectConfig = {
theme: "default",
lang: "zh-CN",
title: "集中一点, 登峰造极⚡"
};
console.log("プロジェクト設定の取得に成功しました");
resolve(projectConfig);
}, Math.random() * 5000)
})
}
async function dispatchGenerator() {
console.log("ユーザー情報の取得を開始します");
const useInfo = await getUserInfoGenerator();
console.log(`取得した${useInfo && useInfo.nickname}ユーザーのプロジェクト設定`, useInfo);
const projectConfig = await getProjectConfigGenerator();
console.log(projectConfig);
}
dispatchGenerator();
両者の書き方の違いは次の通りです:
- Generator 関数の記号 * が async に置き換えられる
- yield が await に置き換えられる
- Generator 関数の自動実行が実行器に依存するのに対し、async 関数は直接その関数を呼び出すことができる
したがって、理論的には async は Generator 関数の構文糖であり、より簡潔な書き方で Generator の実行器を省略しています。理論的には Generator は多かれ少なかれコード内に間接的に存在しているが、直感的には感じられないだけです。