みなさんNode.jsは好きですか?
僕は大好きです。
Javascriptは別に嫌いじゃないけど、非同期処理がねえ...という方もたくさんいるかと思います。
そこで今回はいかにして非同期処理を、そしてコールバック地獄を倒すかという話です。
generatorを使うので古いNode.jsでは動かない気がします。
とりあえず動かしてみる
適当なディレクトリを作って入り、coとthunkifyをインストールして、
index.js
を作って、実行してみます。
$ yarn add co thunkify
# もしくは npm install co thunkify
const co = require('co')
const thunkify = require('thunkify')
const fs = require('fs')
fs._readFile = thunkify(fs.readFile) // 名前が適当なのは許してください
co(function*() {
var file = yield fs._readFile('index.js')
console.log(file.toString())
}).catch(console.error)
さて、これを実行してみましょう。
$ node index.js
# const co = require('co')
# ...
# 以下index.jsの内容が表示される
coとthunkifyとかyieldとは何じゃ
yieldとgenerator
まず7行目にあるfunction*
という記述ですが、これはgeneratorというやつです。
この宣言により作られるのは、functionではなくて、generatorというもので、これは他の言語のgeneratorとかと同じように、複数個の値を、呼び出すたびに1つづつ返すものです。
例えば、1から順に2,4,8,16,32,64...と返すような関数や、
とても長いファイルを呼び出すたびに1行ずつ読み込んで内容を返すような関数なんかが、
典型的にgeneratorといわれて想像するようなものですね。
yieldはreturnと似ているのですが、そのgeneratorを完全に終了するのではなく、一度値を返してgeneratorはその状態で待機するというようなイメージです。
つまり今回のindex.js
では、yield fs._readFile('index.js')
というものの返り値を返却して、このgeneratorは一度停止します。
thunkify
さて、fs._readFile
というのは、何ぞやということで見てみるとthunkify(fs.readFile)
と代入しています。
このthunkifyですが、ざっくりと働きを説明すると、callbackを引数に取るfunctionを、Promiseに変換してくれるものです。
function hoge(a, callback){
setTimeout(callback, 1000)
}
var fn = thunkify(hoge)
fn(1) // これはPromiseが返ってくる
このモジュールは1ファイルしかなくて、とても簡単なので、直接読んでみるといいと思います。
tj/node-thunkify
大体のNode.jsのfunctionはcallbackを利用しているので、これでPromiseに対応させます。
co
最後にcoですが、これはgeneratorがPromiseを返り値として返してきたら、Promiseがresolveするまで待って、その返り値を受け取って、generatorを再実行してくれます。
つまりindex.js
では、readFileにより読み取られたファイルが、yield fs._readFile('index.js')
の評価値となって、再度generatorが再開されます。
これによって、あたかもfs.readFileを同期処理的に書けたことになります。
(readFileSyncと何が違うんじゃいというのは、この後書きます)
ちなみに、fs.readFileのcallbackの引数は(err, data)となっていますが、
第1引数のerrがnullでなかった場合、Promiseがrejectされます。
そしてyieldで帰ってくるのは第2引数以降です。
何とcoも1ファイルです!!
tj/co
しかも書いているのは同じ方ですね!!!
coとthunkifyのありがたみ
並列実行できるぜ!
さっきの例では、fs.readFileSyncしているのと同じでした。
じゃあ、何がいいのかというと、とても簡単に並列に実行して、結果を配列で受け取れます。
$ yarn add request
# もしくは npm install co request
const co = require('co')
const thunkify = require('thunkify')
const request = thunkify(require('request'))
co(function*() {
var response = yield [
request('http://www.google.com'),
request('http://www.yahoo.co.jp'),
request('http://qiita.com')
]
console.log(response)
}).catch(console.error)
何と、これだけで、並列にGETリクエストを送ることができます。
GETリクエストと同時に、ファイル読み込みもできるし、
結果をHashとして受け取ることもできる。
const co = require('co')
const thunkify = require('thunkify')
const fs = require('fs');
const request = thunkify(require('request'))
fs._readFile = thunkify(fs.readFile)
co(function*() {
var response = yield {
qiita: request('http://qiita.com'),
file: fs._readFile('index.js')
}
console.log(response)
}).catch(console.error)
とても便利ですね!!
co.wrap
補足なのですが、coで囲むとそのまま即時実行されてしまいます。
普通のfunctionが欲しいときはどうすればいいかというと、
const co = require('co')
const thunkify = require('thunkify')
const request = thunkify(require('request'))
var parallelRequest = co.wrap(function*(urls) {
var promises = urls.map((url) => request(url))
return yield promises
})
var urls = ['http://qiita.com', 'https://google.co.jp']
parallelRequest(urls).then((arr) => {
console.log(arr)
}).catch(console.error)
これによって、generatorがPromiseを返すfunctionになってくれます。
むむ、この感じ、さっき聞き覚えがありますね。
thunkifyは、callbackをとるfunctionをPromiseを返すfunctionにする。
co.wrapは、generatorをPromiseを返すfunctionにする。
つまり、co.wrapで得られたfunctionもまたyieldできます。
便利ですね!
必ずcatchしよう
Promiseの例外ですが、catchしないとNodeのバージョンによっては握りつぶされることがあります。
割と簡単にデバッグを難しくさせるので、少なくともconsole.log
ぐらいは書いておくようにしましょう。
以上です。
ここ間違ってるよとか、ここ違和感があるよとか、
あと日本語が適当すぎるよなどのツッコミもお待ちしております。