0.はじめに
今回はECMAScript 6から導入されたジェネレータを使ってAsync/Awaitの非同期処理を再現してみたいと思います。
これをやることで非同期処理がどう動いているかやジェネレータの動きの理解が少しでも深まれば良いなと思い勉強がてらやってみました。
ジェネレータやAsync/Awaitの説明は他に書いている方がたくさんいるので割愛します。
早速コードを書いていきます
1.Async/Awaitをまずは使った場合
async function getTweets(userName) {
console.log('Me first')
const data = await fetch(`http://twitter.com/${userName}`)
console.log(data)
}
getTweets('oliver')
console.log('Me second')
このコードの説明を軽くすると、getTweets関数の引数にユーザー名を渡すとそのユーザーが投稿したTweetsが取得できるというシンプルなもの。
注目すべきはawait fetch()の部分でFetch APIを使ってHTTPリクエストを行い、awaitを使ってPromiseの値が取り出されるまで待っています。
無事ツイートが取得できたらその下のconsole.log(data)でレスポンスを出力しています。
2.ジェネレータを使った場合
function doWhenDataFetched (data) {
returnElement.next(data)
}
function* getTweets(userName) {
const data = yield fetch(`http://twitter.com/${userName}`)
console.log(data)
}
const returnElement = getTweets('oliver')
const futureData = returnElement().next()
futureData.then(doWhenDataReceived)
Async/Awaitを使った時より関数が一つ増えコード量が増しました。
まず、最初に先ほどと同様getTweetsでツイートを取得します。
ですが、今回はgetTweets内でawaitはしておりません。
では、どうやってレスポンスが返ってくるのを待ち、その下のconsole.log(data)でFetch APIで返ってきたデータを出力するのでしょうか。
ポイントとなるのはgetTweets前にある*とfetch()前にあるyieldです。
この*を書くことでジェネレータ関数を定義し、yieldキーワードを使ってジェネレータの処理を一時停止しています。
yieldによってawait行ったPromiseの値が取り出されるまで待つを再現しています。
では順番にコード追っていきます。
const returnElement = getTweets('oliver')
ここではジェネレータ関数であるgetTweetsからジェネレータ(ジェネレータ関数から取得できるオブジェクト)を作り、それを定数returnElementにセットします。
注意すべき点は、ここではgetTweetsがまだ実行されていないこと。ただ、ジェネレータを作っただけです。
const futureData = returnElement().next()
ここで先ほど作ったジェネレータを使って実際にジェネレータ関数(元の名前getTweets)を実行します。
ここで理解しておく必要がしておきたいのは
・ジェネレータはイテレータを持つオブジェクト
・このオブジェクトはnext()メソッドをもつ
・next()を実行するとyieldにぶつかるまで関数内の処理が行われる
・next()を実行するとイテレータリゾルトを返す
・再度関数を実行すると前回yieldしたところから処理が再開される
ということ。
これを理解した上でコードを読むとreturnElement().next()を行い関数を実行していることがわかります。
function* getTweets(userName) {
const data = yield fetch(`http://twitter.com/${userName}`)
console.log(data)
}
const returnElement = getTweets('oliver')
const futureData = returnElement().next()
ついに関数が実行されました。
いきなりyieldが登場し、そこでfetchが行われています。
yieldがきたので以下のことがおきます
・処理が一時停止
・イテレータリゾルトを返す
ということはreturnElement().next()からイテレータリゾルトが返され、定数futureDataにセットされます。
この時点でfutureDataはfetchが返すPromiseであることを頭に入れておいてください。
この段階では、Async/Awaitのように
fetchがresolveもしくはrejectするのを待つこと、その下にあるconsole.log(data)がまだ実行しないということができました。
あとは、fetchがツイートを取ってきたらコンソールにデータを出力する処理ができたら再現完了です。
futureData.then(doWhenDataReceived)
futureDataはPromiseであるので.then()を使ってresolveの場合に実行したい処理をセットします。
このresolveの場合に実行したい処理がこちらです。
function doWhenDataFetched (data) {
returnElement.next(data)
}
この関数内では再度next()が登場しています。
ジェネレータ関数であるreturnElementをnext()で実行しています。
ポイントは
・ジェネレータ関数は再度next()で実行すると前回処理を中断した位置から、処理が再開されるということ
・next()の引数にfetchで取得したdataは渡していること
です。
next()に引数を渡すと、その値をジェネレータに渡すことができます。
そのため下記のコードを再度見ると
function* getTweets(userName) {
const data = yield fetch(`http://twitter.com/${userName}`) <=== ここから再開
console.log(data)
}
引数で渡したdataはこのジェネレータ関数内で定義した定数dataにセットされます。
そしてその下でコンソールに取得したdataが出力されます。
3.終わりに
いかがでしたか?
ジェネレータを使ってAsync/Awaitを再現しましたが、結構コード量が増え、なんとなく処理が複雑になった気がします。
Async/Awaitって便利ですね(安易な結論ですが。。)
これを行うことでAsync/Awaitの動きジェネレータの仕組みや動作の理解がほんの少しは深まった気がします。