フロントエンドエンジニアの小松です。
最近個人的にWebアプリを開発する際にPromiseを必要とする場面に遭遇しまして、
せっかくなので備忘録的に執筆したいと思います。
目次
Promiseとは
こちらで確認できます。
以上
カジュアルに理解してみる
…
正直、自分は上記ドキュメントではスッと頭に入ってきませんでしたので、
例のごとくアニメを題材にPromiseなるものをゆるーく説明していきたいと思います。
ところで今期のアニメは『SSSS.GRIDMAN』が話題になっていますね。
作中でグリッドマンはあらゆるアシストウェポンと合体することでパワーアップしますが、その合体シーンは演出と言えども数秒かかっているようです。
敵はヒーローが変身・合体し終わるまでじっと待ってくれるのが定石ですが、これぞまさにPromiseなのではと思いました。
Promiseは主に非同期処理の完了(失敗)のタイミングをスマートにハンドリングできるものと言えます。
敵役の視点からすると、戦闘中に突然いつ終わるのか分からないグリッドマンの合体が始まり、その間はじっと気配を殺し、合体が完了し次第戦闘を再開させるみたいな感じですね。
このやり取りをPromiseで実装してみる
ではこの一連の流れを実際にPromiseで表現してみます。
グリッドマン観てない方には申し訳ないですが、アシストウェポンが擬人化している状態の新世紀中学生はアクセスコードを口にすることでアシストウェポンに変身しグリッドマンと合体することが可能になります。
今回は一番パワーがありそうなバトルトラクトマックスをグリッドマンと合体させてみます。
観てない方は今すぐ視聴するか、とりあえず「アクセスコードを発してから合体完了するまで数秒かかって、合体完了したら決め台詞を口にする」とだけ理解してください。
console.log('勝負だ!グリッドマン!')
let gridman_gattai = new Promise(function(gattai_complete, gattai_failed){
console.log('アクセスコード、バトルトラクトマックス!!')
setTimeout(function(){
gattai_complete('剛力合体超人マックスグリッドマン!! \ ババーン /');
}, 2000);
});
console.log('合体中…');
gridman_gattai.then(function(message){
console.log(message);
console.log('いくぞ〜グリッドマ〜〜ン!!(待ちわびた…)')
}).catch(function(error){
console.log(error);
});
このコードをChromeブラウザのconsoleに貼って実際に実行してもらうと分かりますが、敵が勝負を仕掛けた後にアクセスコードを発し、その2秒後に合体完了しているかと思います。
しっかりと合体の完了を見届けてから襲いかかってきていますね。
一行ずつ解読する
では、このコードを一行ずつ読み進めていきます。
console.log('勝負だッ!グリッドマン!')
console出してるだけなので特に補足することはないですが、おそらくこれはアンチ君だと思います。
let gridman_gattai = new Promise(function(gattai_complete, gattai_failed){
});
Promiseオブジェクトをインスタンス化したものを gridman_gattai
変数に格納しました。
詳細は当記事冒頭のPromiseの仕様を見ていただきいですが、Promiseは resolve
と呼ばれる第一引数と reject
と呼ばれる第二引数をとります。
(厳密にはこの2つの関数を引数に取るexecutor関数というものが存在しますが詳細はドキュメント参照)
処理が完了した際に第一引数(今回は gattai_complete
)を呼び出すことで、Promiseオブジェクトは自身の状態を pending
から fulfilled
に切り替えます(デフォルトは pending
で状態は不可逆です)。
逆に処理中にエラーが発生した場合は第二引数(今回は gattai_failed
)が呼ばれ、pending
から rejected
に変更します。
console.log('アクセスコード、バトルトラクトマックス!!')
同期的に実行された処理です。
setTimeout(function(){
}, 2000);
合体に2秒かかる設定にしました。
今回は説明のためsetTimeoutにしていますが、実戦的にはajaxでデータ取得する系の処理がここに入ることが多いかと思います。
(ところで合体に2秒って早いのか遅いのか…Zガンダムは0.5秒で変形できるそうです)
gattai_complete('剛力合体超人マックスグリッドマン!! \ ババーン /');
setTimeoutの中に記述されているので、2秒経過することで呼ばれる関数です。
(=Promiseが第一引数にとるresolve関数)
この関数を実行することで、後ほど触れるPromiseの then
以下が呼び出されます。
また、引数として 剛力合体超人マックスグリッドマン!! \ ババーン /
と文字列を渡していますが、これは then
関数に渡されます。
ので、ここでしていることは処理が完了したことを通知して、完了後の処理に引数を渡しているです。
console.log('合体中…')
こちらも同期的に実行されるので、setTimeoutで2秒間待機している間に実行されます。
結果、consoleで呼び出される順番として、 アクセスコード、バトルトラクトマックス!!
の後に実行されて合体中を表現できています。
gridman_gattai.then(function(message){
console.log(message);
console.log('いくぞ〜グリッドマ〜〜ン!!(待ちわびた…)')
})...
gridman_gattai
はPromiseオブジェクトのインスタンスを格納していましたね。
setTimeout内のgattai_complete
が実行されることで、このthen
関数が走ります。
そして引数message
にはgattai_complete
から渡された文字列剛力合体超人マックスグリッドマン!! \ ババーン /
が入っています。
あとは同期的に2つのconsoleが記述順に実行され、完了です。
実際にconsoleで実行させる以下のようになるかと思います。
勝負だ!グリッドマン!
アクセスコード、バトルトラクトマックス!!
合体中…
Promise {<pending>}
剛力合体超人マックスグリッドマン!! \ ババーン /
くらえ〜グリッドマ〜〜ン!!(待ちわびた…)
合体失敗したパターン
今回は無事にグリッドマンが合体できたので通過しなかったのですが、Promise内の処理でエラーを投げるとcatch
関数が実行されます。
...
.catch(function(error){
console.log(error);
});
考え方はthen
と同じで、エラーを投げるための関数が存在し、引数を渡すことが可能です。
では実際に合体失敗させてみましょう。
作中で新世紀中学生が総動員してグリッドマンを助けようとした場面で、一度にアシストウェポン化する際に合体失敗してしまうシーンがありました。
(出力サイズが大きいとエネルギーの消費が早いためダメっぽいです)
ので、それを再現してみましょう。
console.log('グリッドマンがピンチだ!みんな、助けに行くぞ!!')
let gridman_gattai = new Promise(function(gattai_complete, gattai_failed){
console.log('アクセスコード、グリッドマンキャリバー!!')
console.log('アクセスコード、バトルトラクトマックス!!')
console.log('アクセスコード、バスターボラー!!')
console.log('アクセスコード、スカイヴィッター!!')
setTimeout(function(){
gattai_failed('うわ〜出力サイズが大きすぎる〜');
}, 2000);
});
console.log('アシストウェポン化中…');
gridman_gattai.then(function(message){
console.log('フルパワーグリッドマン!!')
}).catch(function(error){
console.log(error);
});
基本的な流れは成功パターンと同じなので、異なる点だけ見ていきましょう。
gattai_failed('うわ〜出力サイズが大きすぎる〜');
成功パターンではPromiseの第一引数gattai_complete
を呼び出しましたが、今回のように失敗(エラーハンドリング)の場合は第二引数の関数を呼び出します。
というわけで引数にうわ〜出力サイズが大きすぎる〜
を渡した上でgattai_failed
を呼びだすことでエラーを投げています。
ここらへんは実務ではajaxエラーやAPIからのレスポンスエラーを検知して呼び出す場面が多いかと思います。
}).catch(function(error){
console.log(error);
});
Promiseが正常に実行された場合はthen
が呼び出されますが、エラー時はcatch
関数が実行されます。
gattai_failed
は引数をもってcatch
を読んでいますので、error
をconsoleで出すことでうわ〜出力サイズが大きすぎる〜
を表示します。
実際にconsoleで実行させる以下のようになるかと思います。
アクセスコード、グリッドマンキャリバー!!
アクセスコード、バトルトラクトマックス!!
アクセスコード、バスターボラー!!
アクセスコード、スカイヴィッター!!
アシストウェポン化中…
Promise {<pending>}
うわ〜出力サイズが大きすぎる〜
まとめ
最近はajaxでAPI叩いてフロント側で諸々ハンドリングするのが主流になってきており、Promiseが使いたくなる場面が増えるのではと感じています。
また、実務ではより複雑に非同期処理が絡んでいる場面に出くわすかと思いますが、基本的な考え方は今回の記事と変わらないので、コールバック地獄に陥る前に是非Promiseを選択肢の一つとしていただければと思います!