Node.jsの非同期処理のコールバック地獄を Promise、Generator、async/await を使って解決する

## 同期処理と非同期処理

この記事ではテキストファイルを読み込んでターミナルに出力するというプログラムを通してコールバック地獄の問題点とその解決策を記述します。
実行にはNode.jsのインストールが必要です。

```js:同期的にファイルを読み込む
// Node.jsのFileSystemモジュールを読み込む
const fs = require('fs');
// 同期的にファイルを読み込む
const data = fs.readFileSync(path = 'foo.txt', options = {
    encoding: 'utf8'
});
console.log(data);
```

非同期処理で処理するには読み込みが完了したときに呼び出される関数を用意します。
非同期に処理を行うことで重い処理がほかの処理をブロックすることがなくなり、全体のパフォーマンスが上昇します。

```js:非同期にファイルを読み込む
const fs = require('fs');
// 読み込みが完了したときの処理
const listener = (err, data) => {
    console.log(data);
}
// 非同期で読み込む
fs.readFile(path = 'foo.txt', options = {
    encoding: 'utf8'
}, listener);
```

## 無名関数とコールバック地獄

無名関数を使えば引数に関数オブジェクトを直接指定できるので、同期処理と同じように簡潔に記述できます。

```js:無名関数を使って簡潔に書く
const fs = require('fs');
// 非同期で読み込む
fs.readFile(path = 'a.txt', options = {
    encoding: 'utf8'
}, (err, data) => {
    console.log(data);
});
```

非同期で直列処理を行う場合、コールバック関数を連続して記述します。
しかし、そのまま書くとネストがどんどん深くなり、**コールバック地獄**と呼ばれる読みにくいプログラムになります。

```js:コールバック地獄
const fs = require('fs');
fs.readFile(path = 'foo.txt', options = {
    encoding: 'utf8'
}, (err, data) => {
    console.log('foo.txtを読み込みました', data);
    fs.readFile(path = 'bar.txt', options = {
        encoding: 'utf8'
    }, (err, data) => {
        console.log('bar.txtを読み込みました', data);
        fs.readFile(path = 'buz.txt', options = {
            encoding: 'utf8'
        }, (err, data) => {
            console.log('buz.txtを読み込みました', data);
            ・・・以下略
        });
    });
});
```

このコールバック地獄を回避するために以下のような方法があります。

- **`Promise`**を使う方法（EcmaScript2015～）
- **`Generator`**を使う方法（EcmaScript2015～）
- **`async/await`**を使う方法（EcmaScript2017～）

## `Promise`を使った連続した非同期処理

非同期処理によって得られる値の代わりに、**`Promise`**オブジェクトを返します。
非同期処理が完了した時点で、**`Promise`**オブジェクトから実際の値が得られます。

```js:Promiseを使った非同期処理
const fs = require('fs');

// Promiseを返す関数
const readFilePromise = (filePath) => {
    return new Promise((resolve) => {
        fs.readFile(path = filePath, options = {
            encoding: 'utf8'
        }, (err, data) => {
            resolve(data);
        });
    });
}

// 非同期でテキストファイルを読み込む
readFilePromise('foo.txt')
    .then((data) => { // 非同期処理が完了するたびにthen()メソッドの中に書いた関数が実行される
        console.log('foo.txtを読み込みました', data);
        return readFilePromise('bar.txt');
    })
    .then((data) => {
        console.log('bar.txtを読み込みました', data);
        return readFilePromise('buz.txt');
    })
    .then((data) => {
        console.log('buz.txtを読み込みました', data);
    });
```

## `Generator`を使った非同期処理

Generatorを使用すると反復処理を行うためにのイテレータを簡単に定義にできます。
Generator関数を**`.next()`**メソッドを使って実行すると **`yield`**から次の**`yield`**まで実行され、もう一度**`.next()`**メソッドを実行すると、次の**`yield`**から処理が再開されます。
非同期処理を一時停止し、コールバックが終了したら再開するという処理をすることで、直列処理を実現できます。

```js:Generatorを使った非同期処理
const fs = require('fs');
// 非同期処理の完了を待って関数の続きを呼ぶ関数
const readFileGenerator = (generator, filePath) => {
    fs.readFile(path = filePath, options = {
        encoding: 'utf8'
    }, (err, data) => {
        generator.next(data);
    });
}
// 逐次実行する非同期処理
const generatorFunc = (function* () {
    const a = yield readFileGenerator(generatorFunc, 'a.txt');
    console.log('a.txtを読み込みました', a);
    const b = yield readFileGenerator(generatorFunc, 'b.txt');
    console.log('b.txtを読み込みました', b);
    const c = yield readFileGenerator(generatorFunc, 'c.txt');
    console.log('c.txtを読み込みました', c);
})();
generatorFunc.next();
```

## `async/await`を使った非同期処理

EcmaScript2017からは**`async/await`**を使用できるのでさらに簡潔にコーディングすることができます。
非同期で呼び出したい関数を**`await`**を付けて実行します。
**`await`**を使う関数は**`async function`**として定義する必要があります。

```js:readfile_async.js
const fs = require('fs');
// 非同期でファイルを読み込みPromiseを返す関数を定義
const readFileEx = (filePath) => {
    return new Promise((resolve, reject) => {
        fs.readFile(path = filePath, options = {
            encoding: 'utf8'
        }, (err, data) => {
            resolve(data);
        });
    });
}
// すべてのファイルを非同期に読み込むasync関数を定義
const readAll = async() => {
    const foo = await readFileEx('foo.txt');
    console.log('foo.txtを読み込みました', foo);
    const bar = await readFileEx('bar.txt');
    console.log('bar.txtを読み込みました', bar);
    const buz = await readFileEx('buz.txt');
    console.log('buz.txtを読み込みました', buz);
}
readAll();
```

実際にターミナルで実行してみます。

```powershell
PS C:\> node readfile_async.js
foo.txtを読み込みました *** foo.txt ***
bar.txtを読み込みました *** bar.txt ***
buz.txtを読み込みました *** buz.txt ***
```


**`Promise .then()`**や**`Generator function`**を使うよりもスッキリかけるため、今後は**`async/await`**が主流になっていくようです。この構文はNode v7以降にサポートされています。

## 参考
- [いまどきのJSプログラマーのためのNode.jsとReactアプリケーション開発テクニック](http://www.socym.co.jp/book/1114)
- [非同期処理ってどういうこと？JavaScriptで一から学ぶ](https://qiita.com/kiyodori/items/da434d169755cbb20447)