Promise
コールバックのネスト問題
複数層のコールバック関数が入れ子になっている状態を「コールバック地獄」と呼びます。以下のコードはその例です。
setTimeout(() => { // 第1層のコールバック
console.log('1秒後に出力');
setTimeout(() => { // 第2層のコールバック
console.log('さらに2秒後に出力');
setTimeout(() => { // 第3層のコールバック
console.log('さらに3秒後に出力');
}, 3000);
}, 2000);
}, 1000);
- コールバック地獄の問題点: ⚫ コードの結合度が高すぎて、変更が広範囲に影響を与えるため保守性が低い ⚫ 深いネストにより可読性が低下する
この問題を解決するために、ES6(ECMAScript 2015)ではPromiseという仕組みが導入されました。
Promiseの基本概念
- ① Promiseはコンストラクタ関数です
const p = new Promise()のようにしてPromiseインスタンスを作成でき、これは非同期処理を表します。 - ② Promise.prototypeには**.then()**メソッドが含まれています
各Promiseインスタンスは、
.then()メソッドにアクセスできます。例:p.then() - ③
.then()メソッドは成功・失敗時のコールバックを事前に設定するために使われますp.then(successCallback, errorCallback)p.then(result => { }, error => { })成功コールバックは必須で、失敗コールバックは省略可能です。
then-fsを使ってファイルを読み込む
Node.jsの標準モジュールfsはコールバック形式でのみファイル読み込みに対応しており、Promise形式ではありません。そのため、npm i then-fsコマンドで第三者ライブラリをインストールし、Promiseベースのファイル操作を行う必要があります。
- then-fsのreadFile()は非同期でファイルを読み込み、戻り値としてPromiseオブジェクトを返します。このPromiseに対して
.then()メソッドを呼び出して処理を記述できます。例:
import thenFs from 'then-fs';
thenFs.readFile('./files/1.txt', 'utf8')
.then(r1 => { console.log(r1); }, err1 => { console.log(err1.message); });
thenFs.readFile('./files/2.txt', 'utf8')
.then(r2 => { console.log(r2); }, err2 => { console.log(err2.message); });
thenFs.readFile('./files/3.txt', 'utf8')
.then(r3 => { console.log(r3); }, err3 => { console.log(err3.message); });
前の
.then()が新しいPromiseを返す場合、次の.then()でそれを処理できます。これにより、コールバック地獄を回避できます。
Promiseによる連続したファイル読み込み
// Promiseのチェーンにより、コールバック地獄を解消
thenFs.readFile('./files/1.txt', 'utf8')
.then((r1) => {
console.log(r1);
return thenFs.readFile('./files/2.txt', 'utf8');
})
.then((r2) => {
console.log(r2);
return thenFs.readFile('./files/3.txt', 'utf8');
})
.then((r3) => {
console.log(r3);
});
エラー処理として.catchを使用
Promiseの連鎖の中でエラーが発生した場合、Promise.prototype.catchメソッドでキャッチできます。
- 前のエラーが後の
.then()に影響しないようにするには、.catch()を先に呼び出すべきです。
thenFs.readFile('./files/11.txt', 'utf8')
.catch(err => {
console.log(err.message);
// エラーを処理したため、後続の.thenが実行される
})
.then(r1 => {
console.log(r1); // undefinedが出力される
return thenFs.readFile('./files/2.txt', 'utf8');
})
.then(r2 => {
console.log(r2); // 222が出力される
return thenFs.readFile('./files/3.txt', 'utf8');
})
.then(r3 => {
console.log(r3); // 333が出力される
});
Promise.all()メソッド
Promise.all()は複数の非同期処理を並列に実行し、すべての処理が完了した後に次の.then()を実行します(待機機構)。
const promiseArr = [
thenFs.readFile('./files/1.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/3.txt', 'utf8'),
];
Promise.all(promiseArr)
.then(([r1, r2, r3]) => {
console.log(r1, r2, r3); // 全てのファイルが読み込まれた結果
})
.catch(err => {
console.log(err.message); // エラーがあればここに到達
});
Promise.race()メソッド
Promise.race()も複数の非同期処理を並列に実行しますが、最初に完了した処理のみが次の.then()を実行します(競争機構)。
const promiseArr = [
thenFs.readFile('./files/1.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/3.txt', 'utf8'),
];
Promise.race(promiseArr)
.then(result => {
console.log(result); // 最初に完了した結果が表示される
})
.catch(err => {
console.log(err.message);
});
Promiseを使ったファイル読み込み関数の作成
非同期処理を定義するには、new Promise()の引数にコールバック関数を渡します。この関数内で非同期処理を行い、成功または失敗時にそれぞれresolveまたはrejectを呼び出します。
function loadFile(fpath) {
return new Promise((resolve, reject) => {
fs.readFile(fpath, 'utf8', (err, dataStr) => {
if (err) return reject(err);
resolve(dataStr);
});
});
}
// 使用例:
loadFile('./files/1.txt').then(
success => { console.log(success); },
failure => { console.log(failure.message); }
);