同期処理と非同期処理
この記事ではテキストファイルを読み込んでターミナルに出力するというプログラムを通してコールバック地獄の問題点とその解決策を記述します。
実行にはNode.jsのインストールが必要です。
// Node.jsのFileSystemモジュールを読み込む
const fs = require('fs');
// 同期的にファイルを読み込む
const data = fs.readFileSync(path = 'foo.txt', options = {
encoding: 'utf8'
});
console.log(data);
非同期処理で処理するには読み込みが完了したときに呼び出される関数を用意します。
非同期に処理を行うことで重い処理がほかの処理をブロックすることがなくなり、全体のパフォーマンスが上昇します。
const fs = require('fs');
// 読み込みが完了したときの処理
const listener = (err, data) => {
console.log(data);
}
// 非同期で読み込む
fs.readFile(path = 'foo.txt', options = {
encoding: 'utf8'
}, listener);
無名関数とコールバック地獄
無名関数を使えば引数に関数オブジェクトを直接指定できるので、同期処理と同じように簡潔に記述できます。
const fs = require('fs');
// 非同期で読み込む
fs.readFile(path = 'a.txt', options = {
encoding: 'utf8'
}, (err, data) => {
console.log(data);
});
非同期で直列処理を行う場合、コールバック関数を連続して記述します。
しかし、そのまま書くとネストがどんどん深くなり、コールバック地獄と呼ばれる読みにくいプログラムになります。
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
**オブジェクトから実際の値が得られます。
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
**から処理が再開されます。
非同期処理を一時停止し、コールバックが終了したら再開するという処理をすることで、直列処理を実現できます。
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
**として定義する必要があります。
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();
実際にターミナルで実行してみます。
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以降にサポートされています。