JavaScriptの非同期処理の歴史にGeneratorがあったことも忘れないであげてください
JavaScriptの非同期処理は非常にとっつきにくい概念なので、昔からよく話題になります。
QiitaをJavaScriptでタグ検索すると、大抵誰かしら非同期処理について語っていますし、初心者の嵌りポイントとしても有名です。
さて、そんな非同期処理ですが、最近その扱い方の流れが以下のような形で語られることが多い気がします。
- jQuery時代はコールバック地獄で大変だった
- コールバック地獄を解決するためにPromiseが生まれた
- 最近はasync/awaitによってPromiseをさらに分かりやすく書けるようになった
1~3まで全て言っていることは正しいです。
jQuery時代大変だったのは確かですし、これからのデファクトスタンダードがPromiseになるのもほぼ確実でしょう。
が、2~3年前にはPromiseと並んで次の非同期処理のスタンダードになるぜ!と言われていた技術が実はもう一つありました。
それが、非同期処理とGeneratorの組み合わせです。
世間一般の流れは完全にPromiseなので、今から学ぶ必要があるかと言われるとかなり優先度は下がりますが、
redux-sagaなどこの方式で非同期を管理する現役の仕組みもあります。
なかったかのように扱うのではなく、仕組みくらいはきちんと知っておくべきではないでしょうか。
そもそもGeneratorとはなんぞや
Generator及び、それを作成するためのGenerator関数はPromiseと同じくES2015で定義された機能です。
ブラウザの実装時期もPromiseと似たような感じで(ブラウザによってPromiseより先に実装されたり後に実装されたりしている)ほぼ同時期に使えるようになったと言えるでしょう。
GeneratorはIteratorの一種です。
よって、nextを呼ぶと何らかの値と、処理が完了したかどうかを返します。
//動きの例
hogeGenerator.next();
// {value:"hoge1",done:false}
hogeGenerator.next();
// {value:"hoge2",done:true}
hogeGenerator.next();
// {value:undefined,done:true}
ここまでは普通のIteratorと変わりませんが、GeneratorをGeneratorたらしめているのは、その内部での挙動です。
Generator関数
Generatorは、Generator関数という特別な関数によって作成されます。
先ほど例に挙げたhogeGeneratorは以下のように作成できます。
function* hogeGeneratorFunc(){
yield 'hoge1';
return 'hoge2';
}
const hogeGenerator = hogeGeneratorFunc();
function*
がGenerator関数を定義するための宣言です。
Generator関数を実行すると、Generatorが返されます。
Generator関数を実行しても、定義する際に書いたロジック部分は実行されません。
ロジック部分が実行されるのは、生成されたGeneratorのnext
が呼び出されたタイミングです。
yieldキーワード
Generator関数内ではyieldというキーワードを使うことが出来ます。
Generatorのnextが呼ばれると、Generator関数の先頭から、yieldキーワードに当たるまでロジックが実行され、指定された値がnextの戻り値として帰ります。(上記の例だとhoge1)
その後、nextを呼び出すたびに、前回実行したところから処理を再開し、次のyieldキーワードのぶつかるか、return あるいは関数の末尾に到達するまでロジックを進めていきます。
要するに、yieldとGeneratorを使うことで、一連のロジックとして書いたものの一時停止・再開が出来ます。
Generatorとyieldでどうやって非同期処理を扱うのか
人間、上から下に向かってコードを読むのは得意ですが、左から右へ読むのは苦手です。
よって、非同期処理を挟んでも、上から下に向かって書きたいわけです。
//async awaitでの実現例
async funtion() {
const asyncResult = await asyncFunc();//非同期処理を行いつつ処理を一旦止める
someAfterFunction(asyncResult);//非同期処理が完了した後の処理
}
これを実現するために必要なのは、上から下に向かって流れるコードでありながら、
非同期処理のタイミングで「処理を一時停止」して、非同期処理が終わったタイミングで「そこから再開」する機構です。
そう、まさにgenerator関数内でyieldがやっていることですね。
非同期処理を行うタイミングで「yieldで処理を止めて」、コールバック関数内でgenerator.next()を呼ばせて「処理を再開」することで非同期処理を縦方向で書くことが出来るのです。
具体的には以下のような形です。
function asyncFunc(gen,count){
setTimeout(() => {
gen.next(++count);//非同期処理が終わったらgeneratorのnextを叩く。nextに渡した引数はyieldで取り出すことが出来る。
},1000)
}
const generator = (function* () {
let result = yield asyncFunc(generator,0);//非同期処理を実行しつつyieldで止め、完了後結果を受け取る。
console.log(result);
result = yield asyncFunc(generator,result);
console.log(result);
result = yield asyncFunc(generator,result);
console.log(result);
})();
generator.next();
//1,2,3と1秒間隔で表示される。
この方式で非同期処理を書くのがもっとメジャーだった時代は、
coなどのライブラリを使うことが多かったので実際のコードはもう少しスマートでしたが、原理的には同じです。
結局積極的に使うべきなの?
先述の通り、時代の流れはPromiseなので特別な事情がない限りPromiseでいいと思います。
ただ、非同期処理関係なしにGeneratorは面白い機構なので使いこなせるようになっておくべきかと思います。
まとめ
- Promiseと同時期に、Generatorで非同期処理を書くのが流行りかけた
- 世間の流れはPromiseなので自分から積極的に書くものではない
- 一部この仕組みを使うもの(redux-sagaなど)もあるので原理は知っておくべき
- 非同期処理関係なくGenerator自体は面白い仕組みなので知っておくと良い