はじめに
この記事では、Promiseを全く知らない人がPromiseとは何か、どうして必要なのかを初心者向けに説明します。(実際は公開備忘録です。)
間違っているところありましたら、コメントで指摘してください!
Promiseの学習モチベーション
今どきのJavaScript書くなら知ってて当然だそうです。(( ;゚Д゚))ブルブル
とはいえPromiseを知らなくても十分動くコードは書けるので、必要がなければ後回しにしても良いのでは、とも思います。
誕生した経緯
Promiseは、コールバック地獄を解消するために生まれました。コールバック地獄とは、例えば非同期処理Aの後に非同期処理B、その後に非同期処理Cを行いたいときに発生する、コードが以下のような状態になっていることです。
A(function(){
B(function(){
C(function(){
console.log('Done!');
});
});
});
このように、非同期関数たちを順番に実行したいときなどに、「関数呼び出しのネストが深くなってしまうこと」を、コールバック地獄といいます。
(別に非同期処理を順番に行いたいときに限らず、僕の理解だと要するに関数呼び出しの際の見た目としての深いネストコード=コールバック地獄だと思います)
if文などでもそうですが一般に、ネストは浅い方が人間にとって読みやすいので、コールバック地獄は解消したいものです。
そこで生み出されたのがPromiseです。
このPromiseをうまく使って例と同じことを書くと、だいたい次のように変わります。
A().then(B).then(C).then(function(){
console.log('Done!');
});
変わった点として大切なのは、ネストの深さが小さいということです。
Promiseとは何か
new Promise()の形でインスタンスを作るコンストラクタです。そのインスタンスを本文ではpromiseとして、コンストラクタのPromiseと区別させてください。
実際のコードで主に扱うのはpromise(インスタンス)です。
さて、promiseは何をしてくれるものかというと、批判されにくい形で書けば「作成時点では値が確定していない値を扱えるようにしてくれるもの」です。
具体例の方が分かりやすいので、早速見ていきましょう。
new Promiseに渡した関数は同期的に実行される
let promise = new Promise(function(resolve, reject){
console.log(0);
});
console.log(1);
console.log(promise);
一般にPromiseは非同期関数を利用する場合によく使うので、今回のような非同期処理を含まない例から説明を始めるのは珍しいと思います。
ですが、個人的にこの順番の方が分かりやすいと思うのでこれで説明します。
上のコードをChromeのコンソールで実行した場合、
0
1
Promise {<pending>}
と表示されます。つまり、まずnew Promiseに渡す引数には関数を渡します。そして、渡した関数は同期的に実行されます。
評価の順番としては、
- new Promiseが呼ばれる
- 引数関数が実行される
- new Promiseの値が返る
となっています。
promiseの3つの状態
ここで、promise(Promiseインスタンス)の3つの状態について説明します。
上のpromiseは、全ての実行が終わった後もpending(未完了)状態です。
他には、fulfilled(正しく完了済み), rejected(エラー発生)の状態があります。
Promiseのコンストラクタ引数として渡す関数が呼び出される際には、resolveとrejectという2引数が与えられます。これらが呼ばれると、そのタイミングでそのpromiseはresolvedまたはrejectedになります。resolveかrejectのどちらかが実行されて状態が決定された場合、もうどちらの関数を呼んでもその状態はもう変わりません。
上の例の場合、コンストラクタの引数関数にてresolveもrejectも呼んでいないので、デフォルトのpending状態となっています。
then, catchについて説明1
※少し長たらしいですが、ここは少し理解に時間がかかったので丁寧に説明しています。
then, catchは、Promise.prototype.then、Promise.prototype.catchで定義されています。なので、promiseはいつでもこれらを利用できます。
then, catchには関数を渡して利用し、これらは同期的にpending状態のpromiseを返します。thenやcatchの引数関数は必ず非同期的にしか実行されないので、簡単にthenやcatchは連続して実行することができます。
thenの引数関数は、thenを呼んだpromiseがfulfilledになったタイミングで(非同期的に)実行されます。thenの引数関数が実行されると、then自体が同期的に返していたpromise(チェーンしている場合は変数には保存されていない)がその返り値(何もreturnしていない場合はデフォルトのundefined)でresolveされます。その結果、1つのthenの引数関数の実行が終わると次のthenの引数関数が前のthenの引数関数の返り値を引数として呼ばれます(前にも述べた通りthenの引数関数の引数は呼び出し元のpromiseがresolveした値です。)。一方catchの引数関数は、catchを呼んだpromiseがrejected状態になったタイミングで実行されます。
説明が長くなったので、具体例を見ていきましょう。
(コードは基本的に全てChromeのデバッグコンソールに貼り付ければ実行できるはずなので、是非やってみってください)
then, catchについて具体例3つ
具体例1
同期的にresolveの例。
then(catchも)は、実行されると即時にpendingなpromiseを生成して返します。そのpromiseは、thenを実行したpromiseとは別のPromiseインスタンスです。またthen(catchも)の引数関数は、必ず非同期的に実行されます。
// new Promiseの引数に渡した関数は、promiseの値が確定する前に同期的に実行されます。
let promise = new Promise(function(resolve, reject){
console.log(0);
resolve('これが、引数になります');
});
// thenは同期的にpendingなpromiseを生成して返します。
let promise1 = promise.then(function(message){
console.log(2);
console.log(message); // => これが、引数になります
});
console.log(1);
console.log(promise); // => Promise {<resolved>: "これが、引数になります"}
console.log(promise1); // => Promise {<pending>}
console.log(promise === promise1); // => false
実行結果1
0
1
Promise {<resolved>: "これが、引数になります"}
Promise {<pending>}
false
2
これが、引数になります
具体例2
setTimeoutを用いて非同期的にresolveした例。
基本的に1と同じですが、非同期にresolveしています。また、非同期の例に混ぜる必要はありませんでしたが、resolveが呼ばれた
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve('3秒たった');
}, 3000);
console.log('同期処理はすぐ終了');
});
let promise1 = promise.then(function(message){
console.log(message); // => 3秒たった
console.log(promise); // => Promise {<resolved>: "3秒たった"}
});
console.log(promise); // => Promise {<pending>}
console.log(promise1); // => Promise {<pending>}
console.log(promise === promise1); // => false
setTimeout(function(){
console.log(promise1);
}, 4000);
実行結果2
同期処理はすぐ終了
Promise {<pending>} // promiseはまだpending
Promise {<pending>} // promise1もまだpending
false
// 3秒遅れて
3秒たった
Promise {<resolved>: "3秒たった"}
// その1秒後
Promise {<resolved>: undefined} // promise1を生成したthenの引数関数の返り値はundefinedだったので、undefinedでresolveされた。
具体例3
thenチェーンをつなげた例。
thenの返り値を変数に保存していないため分かりにくくなっていますが、やっていることは同じです。
thenが行われるたびに、(変数に保存していないため)目には見えませんが新しいpromiseが生成されています。
new Promise(function(resolve, reject){
console.log(0);
resolve('a');
})
.then(function(message){
console.log(2);
return message + 'i';
})
.then(function(message){
console.log(3);
console.log(message + 'u');
});
console.log(1);
実行結果3
0
1
2
3
aiu
then, catchについて説明2
いまのところ、promiseを作る方法についてnew Promiseによる方法とthen, catchによる方法の2種類を説明しました。
この2つの作り方の違いについて説明します。
new Promiseで生成したpromiseをfulfilled(またはrejected)にする
new Promiseで生成したpromiseをfulfilledにするには、new Promiseの引数関数のなかでresolveを(同期でも非同期でも)rejectより先に呼べば良いです。
rejectedにするには、rejectをresolveより先に呼べば良いです。
then, catchで生成したpromiseをfulfilled(またはrejected)にする
then, catchで生成したpromiseをfulfilledにするには、その引数関数の実行の中でcatchされない例外が起きなければ良いです。逆にrejectedにするには、引数関数の中で例外を投げれば良いです。
次に、thenとcatchの違いについてです。
thenとcatchの違い
- thenは、その呼び出し元のpromiseがfulfilledになったら実行されます。
- catchは、その呼び出し元のpromiseがrejectedになったら実行されます。
thenとcatchの共通点
- thenもcatchも、引数関数の実行は非同期です。
- thenもcatchも、呼ばれたら同期的に新しくpendingなpromiseを生成して返します。(だからチェーン可能)
- thenもcatchも、引数関数の実行中に例外が発生しなければ、同期的に呼ばれた際に生成したpromiseをfulfilledにします。
- thenもcatchも、引数関数の返り値で同期的に呼ばれた際に生成したpromiseをfulfilledにします。
then, catchについて具体例もう3つ
具体例4
Promiseコンストラクタでpromiseを作る場合は一番最初に述べた通り、引数関数内でrejectメソッドを実行します。
let promise = new Promise(function(resolve, reject){
reject('abc');
});
// 同期的に新しいpromiseを生成して返す
let promise1 = promise.catch(function(message){
console.log(message); // => 'abc'
});
console.log(promise); // => Promise {<rejected>}
console.log(promise1); // => Promise {<pending>}
Promise {<rejected>: "abc"}
Promise {<pending>}
abc
具体例5
一方、thenの返り値のpromiseがrejectedになるのは、thenの引数関数が例外を発生した場合です。
let promise = new Promise(function(resolve){
resolve('abc');
});
let promise1 = promise.then(function(message){
console.log(message); // => 'abc'
throw new Error(message + 'de');
});
let promise2 = promise1.catch(function(err){
console.log(err.message); // => 'abcde';
});
console.log(promise); // => Promise {<resolved>: "abc"}
setTimeout(function(){
console.log(promise1); // => Promise {<rejected>: Error: abcde}
console.log(promise2); // => Promise {<resolved>: undefined}
}, 1000);
abc
abcde
// 1秒後に
Promise {<rejected>: Error: abcde}
Promise {<resolved>: undefined}
promise = new Promise(function(resolve, reject){
resolve('平和な世界')
});
promise.then(function(message){
throw message + 'からの事件発生!'
})
.then(function(){
console.log('resolved状態でないので、実行しないまま次へ')
})
.catch(function(message){
console.log(message)
})
.then(function(){
console.log("実はcatchされると、catchがあらかじめ返していたpromiseはresolveされます。")
});
実行結果5
平和な世界からの事件発生!
実はcatchされると、catchがあらかじめ返していたpromiseはresolveされます。
書ききれなかった備考メモ(本文の繰り返し含む)
- thenの第2引数に関数を入れることでcatchと同じことが出来る。
- then, catchチェーンはpromiseで繋がっている。
- then, catchのチェーンにおいて、最初のnew Promiseをresolvedやrejectedにする方法はresolveやreject関数を呼ぶことだけど、then, catch以降は例外を発生するかorしないか。
- (then, catchのチェーンにおいて)new Promiseの引数関数は同期的に実行されるけど、それ以降は全て非同期実行
- (then, catchのチェーンにおいて)このように、new Promiseの引数関数vsそれ以降でだいぶルールが変わる。
- thenはチェーン上で自分より前のpromiseがresolved状態なら実行され、pendingなら待ち、rejectedならrejectedとしてすぐ返しちゃう。
- catchは呼びだしたpromiseがrejected状態なら実行され、内部で例外が起きなければまたチェーンをresolvedで再開する。
おわりに
もともとPromiseを利用したawait/asyncについて書く予定だったのですが、Promiseでだいぶ長くなってしまったので記事を分けております。
現在、async/awaitの記事は執筆中です。しばらくお待ちください。
参考文献
Promise - JavaScript | MDN
Promise.prototype.then() - JavaScript | MDN
Promise.prototype.catch() - JavaScript | MDN
Qiita 今更だけどPromise入門