Promise
オブジェクトは、非同期に実行されるコールバック処理の結果を受け取るためのプレースホルダとして機能します。
Promise
オブジェクトが提供する then
メソッドを使用すると、Promise
オブジェクトによってラップされたコールバック処理の結果を、別のコールバック処理の入力に繋げることができます。
簡単に言うと、非同期処理 A が終わった後に、その結果を使って非同期処理 B を進める、という処理を分かりやすく記述できるようになるということです(処理結果を次の処理へ繋げることを約束 (Promise) してくれる)。
下記のサンプルコードは、足し算の結果をコールバックのパラメータとして返す簡単な例です(ここでは同期でコールバックしていますが、非同期処理っぽくしたければ、setTimeout
を挟むのもよいでしょう)。
// 足し算の結果をコールバックする
function asyncAdd(val1, val2, callback) {
callback(val1 + val2);
}
asyncAdd(100, 200, function(result) {
console.log(result); //=> 300
});
これくらいであれば、簡潔に記述できますが、非同期処理で受け取った結果を使って次の処理を行う、ということを繰り返そうとすると、下記のように入れ子構造がどんどん深くなるというコールバック地獄にハマります。
asyncAdd(100, 200, function(result) {
asyncAdd(result, 300, function(result) {
asyncAdd(result, 400, function(result) {
asyncAdd(result, 500, function(result) {
console.log(result); //=> 1500
});
});
});
});
そこで、Promise
オブジェクトの出番です。
function asyncAdd(val1, val2) {
return new Promise(resolve => {
resolve(val1 + val2);
});
}
asyncAdd(100, 200).then(result => console.log(result)); //=> 300
Promise
を使って処理結果を受け取れるようにするには、パラメータとしてコールバック用のオブジェクトを受け取るのではなく、上記の asyncAdd
関数のように、Promise
オブジェクトを返すように実装します。
そして、実際の処理結果は Promise
オブジェクトのコンストラクタで渡された resolve
コールバックメソッド経由で返すようにします。
こうしておくと、Promise.then
を使用することで、非同期処理の結果をコールバックで受け取ることができるようになります。
上記はシンプルな例なので大した効果はないのですが、コールバック処理を連鎖させた処理を記述するときに効果を発揮します。
次のコードは、asyncAdd
の処理結果を、次の asyncAdd
の処理に繋げるということを繰り返し行っています。
asyncAdd(100, 200)
.then(result => asyncAdd(result, 300))
.then(result => asyncAdd(result, 400))
.then(result => result + 500)
.then(result => console.log(result)); //=> 1500
このような記述方法を Promise チェーンと呼び、ES6 時代のコールバック処理の記述方法として広まってきています。
ここで、注目したいのは 3 つ目の then
呼び出しで、コールバック処理の結果として result + 500
という単純なスカラ値を返しており、Promise
オブジェクトを返していないことがわかります。
それなのに、次の then
メソッド呼び出しにチェーンすることができているのは、then
メソッドがコールバック処理の戻り値を Promise
オブジェクトにラップして返してくれているからです。
つまり、Promise チェーンを記述するには、チェーンの最初だけ Promise
オブジェクトになっていればよいことになります。
function asyncAdd(val1, val2) {
return new Promise((resolve) => {
resolve(val1 + val2);
});
}
という resolve
コールバックだけを扱う Promise
オブジェクトを生成するときは、次のように Promise.resolve
を使って簡潔に記述することができます。
function asyncAdd(val1, val2) {
return Promise.resolve(val1 + val2);
};
実行したい非同期処理に、成功と失敗の2種類の結果があるケースでは、Promise
による非同期処理のチェーンがさらに役に立ちます。
例えば、下記のようなファイルダウンロードのための疑似関数 fetchFile
があるとします。
シンプル化のため、fetchFile
は .png
拡張子を持つファイルを要求された場合のみ成功することとします。
function fetchFile(url, successCallback, failureCallback) {
if (url.endsWith('.png')) {
successCallback('success: ' + url);
} else {
failureCallback('failure: ' + url + ' not found');
}
}
この(疑似非同期)関数を使い、次のようなファイルを順番に取得していき、取得に失敗した時点で処理を止めることを考えてみます。
file1.png
(取得成功する)file2.png
(取得成功する)file3.jpg
(ここで取得に失敗する)従来のコールバックの仕組みを使用して、fetchFile
を実行するプログラムは下記のようになります。
function myFailureCallback(err) {
console.error(err);
}
fetchFile('file1.png', function(result) {
console.log(result);
fetchFile('file2.png', function(result) {
console.log(result);
fetchFile('file3.jpg', function(result) {
console.log(result);
}, myFailureCallback);
}, myFailureCallback)
}, myFailureCallback)
success: file1.png
success: file2.png
failure: file3.jpg not found
このように、コールバックの連鎖にエラー処理を加えると、さらに悲惨なコールバック地獄に陥ります。
上記の例では、エラーハンドラとして共通の myFailureCallback
を使用しているのにもかかわらず、3 箇所もそのエラーハンドラを渡しています。
非同期関数を Promise
化することで、このようなエラー処理もスッキリ記述できるようになります。
Promise
コンストラクタで渡されるコールバック関数の第2パラメータ reject
はコールバック関数となっており、これを呼び出すことで非同期処理が失敗したことを示します。
下記は、上記のプログラムを Promise
化した例です。
function fetchFile(url) {
return new Promise((resolve, reject) => {
if (url.endsWith('.png')) {
resolve('success: ' + url);
} else {
reject(new Error(url + ' not found'));
}
});
}
fetchFile('file1.png').then(result => {
console.log(result);
return fetchFile('file2.png');
}).then(result => {
console.log(result);
return fetchFile('file3.jpg');
}).then(result => {
console.log(result);
}).catch(reason => {
console.error(reason);
});
いずれかの非同期メソッドの実行が失敗に終わり、reject
が呼び出されると、非同期メソッドの呼び出し側では Promise.catch
メソッドが呼び出されて Promise チェーンの実行が止まります。
エラー処理の内容はコールバック関数として実装し、Promise チェーンの最後の Promise.catch
に渡してやります。
上記のエラー処理は、ちょっとシンプルすぎかもしれませんが、Promise
を使って非同期処理をチェーンさせることで、エラーハンドル処理に関しても読みやすく記述できるようになることが分かると思います。
Promise
のインスタンスを生成すると、コンストラクタに渡されたコールバック関数が直ちに実行されます(正確には Promise
オブジェクトがインスタンス化されるよりも前に実行されます)。
つまり、ある非同期関数を Promise 対応させたい場合は、単純にその関数の中身をすべて Promise
コンストラクタに渡すコールバック関数の中に移動させてしまえば OK です。
function hoge() {
return new Promise((resolve, reject) => {
// ...
// これまでと同様非同期処理
// ...
resolve(result);
});
}
hoge().then(result => console.log(result));
上記のようにしておけば、hoge()
を呼び出した直後にこれまでと同様の非同期処理が開始され、Promise.then
のコールバックを介して結果を受け取れるようになります。
Promise.then
を呼び出したときに非同期処理が開始されるわけではありません。
あくまで非同期処理が開始されるタイミングは Promise
を導入する前と変わらないということです。