初めに
今まで非同期処理の実装はasync
await
を使用してきましたが、根本的な理解ができておらず非同期処理の実装の度に頭にハテナがついて回っています。なのでまずはPromiseの理解を深め、まとめていきたいと思います。
Promiseとは
Promise オブジェクトは非同期処理の最終的な完了処理 (もしくは失敗) およびその結果の値を表現します。
PromiseはJavaScriptのビルトインオブジェクトであり、JavaScriptが動作する環境であればどこからでも呼び出すことができます。その実体は非同期処理を抽象化したオブジェクトであり、非同期処理を操作するための仕組みでもあります。
またPromiseはコンストラクタ関数であり、オブジェクトを生成、使用する際にはnew
でインスタンス化します。
const promise = new Promise((resolve, reject) => {
asyncFunc(){...}
});
コールバック関数との比較
非同期処理といえばコールバック関数で行う方法があると思います。
asyncFunc(arg, (error, result) => {
// 最初に実行されるべき処理
if (error) { // コールバック失敗時の処理
throw error;
}
// コールバック成功時の処理
});
asyncFuncを実行した時、asyncFunc関数内の最初に実行されるべき処理が完了した後に第二引数で指定したコールバック関数が実行され、処理順番の担保を取ることができます。
コールバック地獄
上記のようにコールバック関数が1つであれば、まだ簡潔で処理が追いやすいかと思います。しかし、非同期処理が何個もある場合、コールバック関数では入れ子が深くなり何をやっているのか一見した限りではわからないということがあります。このような問題をコールバック地獄
といいます。
syncFunc((data) => {
// 初めに実行される処理
nextFunc((data) => {
// 次に実行される処理
nextNextFunc((data) => {
// 次の次に実行される処理
finalFunc((data) => {
// 次の次の次に実行される処理
}); // finalFunc
}); // nextNextFunc
}); // nextFunc
}); // syncFunc
実際は一つ一つの処理が何行かに渡って書かれるわけですから1つの関数がかなり肥大化してしまい、これに後から手を付けるとなったら、少しだけ現実から目を背けたくなるかと思います。
これをPromiseで記述すると以下のようになります。
function asyncFunc(data) {
return new Promise((resolve, reject) => {
// 初めに実行される処理
resolve(hoge);
});
}
// nextFunc以下もasyncFuncと同様にPromiseオブジェクトをreturnする
...
asyncFunc(data).then((result) => {
return nextFunc(data);
}).then((result) => {
return nextNextFunc(data);
}).then((result) => {
return finalFunc(data);
})
Promiseを使用する場合、非同期処理をasyncFunc
のように関数としてまとめます。その関数では、Promiseオブジェクトが戻り値として返されるように記述します。その後の処理の流れはまた次回にまとめたいと思いますが、軽く説明するとPromise内でresolveされた場合、Promiseのインスタンスメソッドであるthen
に処理が移ります。そしてそのthenメソッドの中で次に実行したいnextFunc
関数を実行します。省略していますがnextFunc
関数でもPromiseオブジェクトをreturnし、Promise内でresolveされ、次のthenメソッドに移る、という流れになりこうして実行の順番を確保することができます。
コールバック関数と比較すると、一つ一つの処理が小分けにされ何が返ってきてから次の関数が実行されるかが明確になり、修正を加える時などに手をつけやすくなったかと思います。処理の順番も追いやすいですね。
書き方
コールバック関数は書き方に明確なルールはありません。自由に記述することができます。ただ、それ故に統一性が失われてしまう可能性もあります。先程のソースコードをもう一度確認してみましょう。
asyncFunc(arg, (error, result) => {
// 最初に実行されるべき処理
if (error) { // コールバック失敗時の処理
throw error;
}
// コールバック成功時の処理
});
JavaScriptでのコールバック関数の第一引数にはError
オブジェクトを渡すという慣例があります。ただし、先述した通りコールバック関数には明確なルールはなく、この慣例はあくまでコーディングルールなので開発者によって様々な書き方になります。
対してPromiseには、コールバック地獄の例でも見たように、成功時の処理と失敗時の処理のそれぞれに関数が用意されており、その用意されているメソッドthen
やcatch
以外は使用することができないので、書き方が統一されます。つまり、Promiseで書き方が統一され、Promiseによる様々な非同期処理のパターン化できることがPromiseのメリットの1つになるでしょう。
まとめ
まだPromiseに関して序盤もいいとこですが、思ったより長くなってしまったので次回投稿に回したいと思います。初めにも書きましたが開発の非同期処理でいきなりasync
await
を使って実装したため、理解が足りていないところが多く応用がきかないという状況に陥っていたので今回の記事をまとめさせてもらいました。既に実装で使っているからか、このまとめだけでも今まで疑問に感じていたところが解消されたので良かったです。ただ、Promiseの理解、という観点としてまだまだ足りなさ過ぎるので、次回も引き続き同じテーマで学習していきたいと思います。