es6のgeneratorやyieldやpromise周りの振る舞い
前回で環境構築は終わったけれど……
最後にgeneratorやyieldやpromise周りの振る舞いを一度勉強し直しておこう。たびたび使っているのに振る舞いがよくわからないし。
おさらい
とりあえず今までの使い方からして、yieldを頭に付けておくことで呼び出した関数が終了するまでcodeが待機してくれるというのはわかる。同期的な処理。
ただpromiseとgeneratorって別物なのかとか、yieldで止めているというのはどういう状態を意味しているのかとか、わからない。
そこかな。
まずはpromiseについて
Promiseは非同期処理を抽象化したオブジェクトとそれを操作する仕組みのことをいいます。
んではgeneratorは?
Generator の特徴は、処理を任意の時点で止めてそこから再開できることです。 これを上手く用いると、非同期処理の実行で一旦止め、コールバックが終了したら再開するということができます。
この時、 yield と next() で値をやりとりできるので、コールバック内でしか触れなかったデータ (err, data など) を、コールバックの外の世界に出すことができるようになりました。
なるほど。振る舞いとしての理解はあっている模様。
でもまだいまいち理解しきれてない感ある。promiseとgeneratorの差とか。いやここで話していることを見ればぼんやりとはわかるけど。ぼんやりじゃ嫌なんじゃ。
ES6のGeneratorとは「任意の時点で処理を中断/再開することができる関数」というもの。一般的にはコルーチン(coroutine)と呼ばれるもので、サブルーチン(通常の関数)を一般化したもの。ES6でGeneratorを理解するには3つのキーワードがある。
Generator Function 処理の中断/再開が行われる特殊な関数 function generatorFunction(){}のようにfunctionを使って定義する
Generator Object 中断された処理を再開したり、値を取得し対するオブジェクト var generatorObject = generatorFunction()のように取得する
yield Generator Functionの中で使われる処理の中断を指定するキーワード var result = yield request(‘http://example.com’)のように使う
あー。yieldって処理の中断という意味だったのか。
function *という構文により生成された関数generatorFunctionはGenerator Functionとなる。そしてgeneratorFunctionを実行すると処理が 開始されるのではなく Generator Object generatorObjectが返される。
generatorObject.next()を実行することで関数本体の処理を開始することができる。処理が開始され、yield式が実行されるとそこで 処理が中断 される。中断された処理を再開するにはgeneratorObject.next()を実行する。そうするとまた次のyield式で処理が中断される。
ああ。するとredux-sagaもどこかでnextを実行しているのか。それに引きずられて、という感じかな。
あー……jsで並列処理している背景がなんとなくわかった気がする。
いやでもまてよ。
function* getHandleRequest() { yield takeEvery("TEST_JSON_REQUEST", TestJSON); } function* testHandleRequest() { while(true){ yield TestTimer(); } }
これ、getHandleRequestはまだしも、testHandleRequestはnextしても意味があるのか?
なお中のTestTimerはこう。
export default function* TestTimer() { yield sleep(1000).then(function(){}); let today = new Date(); yield put({type:"TIMER_UPDATE", value:today.toString()}); }
sleepで止まってputで止まって。どういう処理になってる?
「yieldを使えば関数の実行を途中で止められる」→「それならフロー制御の本体をgenerator関数で書けばよい?」→「yieldで非同期処理が終わるのを待って、非同期処理が終わったらg.next(val)で処理を戻す」→「ネストなしで書けてうれしい」というのが、generatorによる非同期処理の基本的な発想。
yieldしたのがpromiseだと、非同期処理が終了するまで待機、の意味になるのかな。
forkしたtask風に振る舞わせるやり方はredux-sagaの中できっと実装しているんだろうけど(待機中のtaskを見つけたら別のを実行する的な)。でないとwhile(true)させているのに並行して他の処理も実行されている理屈がわからなくなる。
話を戻して。
yieldの右辺値を制御側で受け取るにはgen.next()の戻り値result.valueから取得する。
とすると、
const [payload,error] = yield call(getJSON, action.url);
これは実質的にcallでwrapしているだけでgen.next()的な物だったりするのかな。
redux-sagaの中身を見ているとちょくちょくnext()とか出てくるけど。
まずgen.next()によりyield sleep(1000).then(cb)の右辺までが実行され、非同期処理sleep(1000)の開始とPromiseにコールバックを設定するthen(cb)までが行われて中断する。そしてsleep(1000)が完了すると、Promiseに登録したcbが実行される。ここでcbは制御側から与えられた関数であり、内部はgen.next()を実行しており、処理を再開される。これで1秒間停止したあとに、処理が再開される非同期処理を同期処理のようにかけるようになった。
へー。なるほどなるほど。そうか。generatorをpromiseの中に渡せばnextを実行できるしgeneratorの振る舞いと一致するな。
非同期処理の戻り値をyieldの右辺に渡すことで、戻り値がある非同期処理にも対応できるようにする。これは単純にPromise#resolveに渡される値をgen.next()で渡せばいいだけ。
ほほう。そうだったのか。ちなみにresolveって何かというと。 http://azu.github.io/promises-book/#ch2-promise-resolve を見ればすぐにわかる。単にPromiseを即時実行してresolveを実行するだけ。
んー。まだよくわからないけどyield使うとpromiseの実行終了まで待機してくれるようになるし、generatorのreturn valueと同様と見なしてresolveの中身を取ってきてくれる、と。
だからredux-sagaの中でやたらgeneratorが出てきたのも、非同期処理を任意のtimingで実行したかったから、という感じ?
で、実際に実行されているのは非同期処理だからblockingされていないし……? いやでもgeneratorに落とし込んだ時点でされているよな。あれ? それともここでpromise使った並列処理?
generatorはJavaScript向けの軽量版のコルーチンです。yieldキーワードを使って関数を一時停止したり、再開したりできます。generator関数はfunction* ()という特殊な構文を使います。その威力をもってすれば、promiseや「サンク」を使った非同期処理を一時停止したり、再開したりすることもできるのです。 つまり”同期風”の非同期コードが書けるというわけですね
同期風の非同期コード……。
いやgeneratorを利用すれば非同期処理の間止めておくみたいなまねが出来るのはわかるけど。
あ。いやそういうことか。
非同期処理の開始時にyieldで止めてしまえば、非同期処理が実行される一方、それをwrapしているgeneratorはnextが呼び出されるまでどうあろうと止まったままだ。というと流れとしては、
- generatorが実行される
- 非同期処理開始
- generatorから一度抜ける
- なんかしている (2の非同期処理は非同期処理なので他をblockingしないで処理続行中)
- 1のgeneratorのnextを実行する
みたいな流れか。4というか非同期処理が終わってない場合にnextを呼び出したらどうなるのかよくわからないけど。
Generatorとは、端的に言うと関数の一時停止、再開ができる機能です。通常の関数は常に最初から実行され、returnが発生するまで一気に処理されますが、Generatorではその処理の途中で一度関数を抜けたり、また一時停止中の位置から処理を再開したりできます。一時停止、再開する際はパラメータの受け渡しも可能です。
先ほど述べた通り、通常の非同期関数では
- 「処理が終わったら実行する関数」をあらかじめ渡しておく
- 処理が終わったらその関数を呼び出す
というインターフェースを持ちますが、これを
- 「処理が終わったら再開するGenerator」をあらかじめ渡しておく
- 非同期関数を実行すると同時にGeneratorの処理を一時中断する
- 処理が終わったらそのGeneratorを再開する
という形に書き換えればよいのです。
なるなる。そんなに単純に再開してくれるならさっきの流れも特に問題なく動く。
ここで変遷を見て大体納得できた。
ジェネレーターは処理を抜け出すことも後から復帰することもできる関数です。ジェネレーターのコンテキスト (変数の値)は復帰しても保存されます。
ジェネレーター関数を呼び出しても関数は直ぐには実行されません。代わりに、関数のためのiterator オブジェクトが返されます。iterator の next() メソッドが呼ばれると、ジェネレーター関数の処理は、イテレーターから返された値を特定する最初のyield演算子か、ほかのジェネレーター関数に委任する yield*に達するまで実行されます。next() メソッドは生産された値を含む value プロパティとジェネレーターが最後の値を持つかを示す done プロパティを持つオブジェクトを返します。
yield 式によって実行が停止されると、ジェネレーターの next() メソッドが呼び出されるまで、ジェネレーターのコード実行は一時停止します。ジェネレーターの next() メソッドが呼ばれるたびに、ジェネレーターの実行が再開され、次のうちのいずれかに達するまで実行されます:
- ジェネレーターを再び停止して、ジェネレーターの新しい値を返す yield。再度 next() が呼ばれると yield の直後から実行が再開されます。
- ジェネレーターから例外をスローするために使用される throw。完全にジェネレーターの実行を停止し、例外がスローされたときに通常そうであるように呼び出し元で実行が再開されます。
- ジェネレーター関数の終わり: この場合、ジェネレーターの実行は終了し、value に undefined が、done に true が代入された IteratorResult オブジェクトが呼び出し元に返ります。
- return ステートメント。この場合ジェネレーターの実行は終了し、value が return ステートメントで指定した値で done が true の IteratorResult オブジェクトが呼び出し元に戻されます。
ジェネレーターの next() メソッドにオプションの値が渡された場合、その値はジェネレーターの現在の yield 操作の返り値となります。
ここの話の流れが今にしてみるとわかりやすい。
まあ当初の目的である振る舞いは理解できたからこれでいいか。
余談: renderの並列化
あ。reduxでの初期描画の時、promise.allでくるんであげればroot treeから並列でdom描画できるんじゃないか? 前にもやって挫折したけど。
こうしてみた。
let ps = []; for(let name in this.render){ ps.push(new Promise(function(resolve,reject){ ReactDOM.render( <Provider store={store}> <Router history={history}> {this.render[name].dom} </Router> </Provider>, this.render[name].bind ) })) /* ReactDOM.render( <Provider store={store}> <Router history={history}> {this.render[name].dom} </Router> </Provider>, this.render[name].bind * )*/ } Promise.all(ps).then(function(value) { console.log("ok"); }, function(reason) { console.log(reason) });
errorが出た。TypeError: Cannot read property ‘render’ of undefinedだそうな。
あああ、thisのせいか。おのれscope。
ps.push(new Promise((resolve,reject) => { ReactDOM.render( <Provider store={store}> <Router history={history}> {this.render[name].dom} </Router> </Provider>, this.render[name].bind ) }))
こう修正。今度はerrorなしで描画してくれる。
が、blcokingしてしまうな。
根本的にrenderが非同期な処理じゃないからいくら非同期関数でwrapしようとその処理が実行されたときに止まるって話か。しゃあない。
ああそれでReact Fiberに期待という話になる訳ね。一周回ってしまった。
いつか自前でちゃんとgeneratorを組んでみたいな。まだ使う機会が見えないけど、割とそう遠くないうちに来そうな気もする。