LoginSignup
23
26

More than 5 years have passed since last update.

Node.jsでの非同期処理を、coとthunkifyを使って幸せに書こう

Last updated at Posted at 2016-11-27

みなさんNode.jsは好きですか?
僕は大好きです。

Javascriptは別に嫌いじゃないけど、非同期処理がねえ...という方もたくさんいるかと思います。
そこで今回はいかにして非同期処理を、そしてコールバック地獄を倒すかという話です。

generatorを使うので古いNode.jsでは動かない気がします。

とりあえず動かしてみる

適当なディレクトリを作って入り、coとthunkifyをインストールして、
index.jsを作って、実行してみます。

shell
$ yarn add co thunkify
# もしくは npm install co thunkify
index.js
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)

さて、これを実行してみましょう。

shell
$ 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に変換してくれるものです。

thunkifyのイメージ
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しているのと同じでした。
じゃあ、何がいいのかというと、とても簡単に並列に実行して、結果を配列で受け取れます。

shell
$ yarn add request
# もしくは npm install co request
parallel.js
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として受け取ることもできる。

index.js
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が欲しいときはどうすればいいかというと、

co-wrap.js
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ぐらいは書いておくようにしましょう。

以上です。

ここ間違ってるよとか、ここ違和感があるよとか、
あと日本語が適当すぎるよなどのツッコミもお待ちしております。

23
26
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
26