JavaScriptのPromiseって何?を解決したい

JavaScriptのPromiseなんとなく使っているけど実は理解できていないかもという人やJavaScriptを始めたばかりの人を対象に一人でもPromiseを理解してもらえるように身近な例とシンプルなコードを使ってPromiseの説明を行っていきます。
Promiseを説明する場合は同期や非同期といった言葉と一緒に解説されることが多く、Promiseの理解よりも非同期部分の説明で混乱している人が多いのではないでしょうか。Promiseを理解したい場合は、同期、非同期とPromiseの関係を一度忘れることがおすすめです。そうすればPromise自体は非常にシンプルであることがわかります。
目次
JavaScriptのPromiseとは
身近にありそうな例を使ってPromiseとはどんなものかを理解していきましょう。
Promiseという英単語の日本語訳を学生時代に”約束”と覚えた人も多いかと思います。プログラムで使われる機能の名前は、その名前の意味を元につけられているので”約束”という意味を念頭においてPromiseを確認していきます。
明日友人と近所のカフェで10時に待ち合わせの約束をします。約束には、約束通り10時に行く場合と約束を破り10時に行けない場合の2つの結果が存在します。Promiseも日常的な約束と同様に約束を守る結果(これをPromiseではresolve)と約束を守らない結果(これをPromiseではreject)の2つの結果を持ち、どちらかの一方の結果を戻します。
Promiseのコード化
”Promiseが2つの結果を持ちどちらかの一方の結果を戻す”とはどういうことか確認するためにPromiseの実際のコードを使って解説していきます。
Promiseオブジェクトをnew演算子を利用してインスタンス化し変数yakusokuに代入します。
let yakusoku = new Promise()
Promiseの引数には2つのコールバック関数resolveとrejectが入ります。
let yakusoku = new Promise(function(resolve,reject){
//ここに処理を記述する
})
最後に約束を守った場合と守らなかった場合のコードを追加します。 約束を守った場合はresolveに値を入れ、守らなかった場合にはrejectに値を入れます。
const keep_promise = true
const yakusoku = new Promise(function(resolve,reject){
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
})
これでPromiseで約束をコード化することは完了です。
100%約束通りにカフェに到着すると言い切れる人なら下記のようにresolveのみを記述することも可能です。
const yakusoku = new Promise(function(resolve){
resolve('いつも約束通りつくよ。')
})
同じ処理を関数を使って記述することもできます。
const keep_promise = true
const yakusoku = function (){
return new Promise(function(resolve,reject){
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
})
}
また関数を使えばこちらの記述でも問題ありません。
const keep_promise = true
function yakusoku() {
return new Promise(function(resolve,reject){
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
})
}

Promiseを使う(約束を守った場合)
Promiseを作成することができたのでこのPromiseを使って約束を守った場合の動作確認を行なっていきます。作成した変数yakusokuにthenをつけることでPromiseのresolveの結果を取得することができます。thenの中には関数が入り、その引数にはresolveの結果が入ります。
keep_promiseの値はtrueで約束を守った場合です。
// keep_promise = trueの場合
yakusoku.then(function(comment){
console.log(comment)
})

実行すると”約束通りついたよ。”が表示されます。
約束通りついたよ。
文字列ではなくresolveに入れる値が配列の場合はどのようになるかも確認しておきましょう。
if (keep_promise){
resolve(['a','b','c'])
} else {
reject('約束破ってごめん。')
}
結果は、配列が取得できます。
[ 'a', 'b', 'c' ]
Promiseを使う(約束を守らなかった場合)
約束を守らなかった場合(keep_promise = false)はどのようにrejectに入れた値を取得するのか確認します。
約束を守らなかった場合はthenではなくcatchを使うことでrejectの値を取得することができます。
// keep_promise = falseの場合
yakusoku.then(function(comment){
console.log(comment)
})
.catch(function(comment){
console.log(comment)
})
実行すると”約束破ってごめん。”が表示されます。
約束破ってごめん。
catchを利用しなくてもthenに2つの関数を入れて、rejectの値を取得することもできます。
yakusoku.then(function(comment){
console.log(comment)
},
function(comment){
console.log(comment)
})
})
上記では約束を守るとthenの最初の関数が実行され、約束を破ると2つ目の関数が実行され、コメントが表示されます。
2つの関数の引数に同じ名前が使われているためわかりにくい場合は下記のように記述することも可能です。(rejectを含めなくてもよい)
yakusoku.then(function(result){
console.log(result)
},
function(error){
console.log(error)
})
})
Promiseのステータスとは
Promiseは、3つのステータスを持っています。pending、resolved, rejectedの3つです。ステータスの状態を視覚的にも理解するためにChromeのデベロッパーツールを利用します。
これまでに出てきたresolveとrejectからresolvedは約束を守ってresloveの値が戻された場合のステータスに関係し、rejectedは約束が守られずrejectの値が戻された場合のステータスに関係するというのが予想できます。
ChromeのデベロッパーツールでresolveとrejectのPromiseのステータスを確認することができます。


Pendingステータスとは
もうひとつステータスpendingがどのようなものか確認していきます。pendingは決定を待っている状態という意味があり約束の例を使うと約束の時間がまだ来ていない状態です。つまり約束を守るresolveなのか守れないrejectのかわからない状態です。
pendigの状態を確認するためにsetTimeout関数を利用して、5秒後にresolveとrejectの結果を取得するように変更を行います。
const keep_promise = false
const yakusoku = new Promise(function(resolve,reject){
setTimeout(function(){
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
},1000*5)
})
デベロッパーツールを見ると上記では5秒後にresolveかrejectが戻されるのでその間はpendingのステータスとなります。

ここまでの動作確認でPromiseの3つのステータスの意味が理解できたのではないでしょうか。
Promiseを連結する方法
2つのPromiseを連結
Promiseはチェーンでつなげて処理連結することができます。連結する方法を確認していきます。
連結を行うことで一つの処理を行った後にその処理の結果を次の処理に渡すことができるようになります。
新たにcatchTrainという変数を作成し関数を設定します。関数は引数を受け取りreturnでPromiseを戻します。on_scheduleという変数の値によってresolveで戻すかrejectで戻すかが決まります。

const catchTrain = function(comment){
return new Promise(function(resolve,reject){
if (on_schedule){
resolve(comment + '10時2分の電車に乗ろう!')
} else {
reject(comment + 'でも今日は電車遅れてるね。')
}
})
}
先程作成したyakusokuのコードにcatchTrainを追加します。約束通りに到着して電車の出発時間が定刻通りかどうかで表示される内容が異なります。
const keep_promise = true
const on_schedule = true
const yakusoku = new Promise(function(resolve,reject){
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
})
//関数の場合
//const yakusoku = function() {
// return new Promise(function(resolve,reject){
// if (keep_promise){
// resolve('約束通りついたよ。')
// } else {
// reject('約束破ってごめん。')
// }
// })
// }
const catchTrain = function(comment){
return new Promise(function(resolve,reject){
if (on_schedule){
resolve(comment + '10時2分の電車に乗ろう!')
} else {
reject(comment + 'でも今日は電車遅れてるね。')
}
})
}
実行する際は下記のようにthenを利用して連結することができます。catchTrainはyakusokuのresolveの値を受け取り内部の処理を実行します。catchTrainの処理でresolveの値が戻されたらその下のthenが実行され、console.logにコメントが表示されます。yakusokuとcatchTrainのどちらでrejectが戻されてもcatchが実行されます。

//関数の場合
// yakusoku().then(catchTrain)
yakusoku.then(catchTrain)
.then(function(comment){
console.log(comment)
})
.catch(function(comment){
console.log(comment)
})
keep_promiseとon_scheduleの値によって表示される内容が異なるので確認していきましょう。
どちらもtrueの場合は下記のように表示されます。
//const keep_promise = true
//const on_schedule = true
約束通りついたよ。10時2分の電車に乗ろう!
on_scheduleがfalseの場合は下記のように表示されます。
//const keep_promise = true
//const on_schedule = false
約束通りついたよ。でも今日は電車遅れてるね
keep_promiseがfalseの場合はyakusokuが実行されrejectで値が戻されるためcatchTrainを経由せずにcatchが実行されます。
//const keep_promise = false
//const on_schedule = true
約束破ってごめん
//const keep_promise = false
//const on_schedule = false
約束破ってごめん
このようにPromiseは連結することにより1つ目の処理(yakusoku)の結果が次の処理(catchTrain)に渡されていることがわかりました。
シンプルな関数で複数のPromiseを連結
1秒後に入力した数字を1足して戻すというPromiseの関数を作成します。
const addNumber = function(number) {
return new Promise(function(resolve){
setTimeout(() => {
resolve(number + 1);
}, 1000);
});
};
addNumberの引数に1を入れて実行すると1秒後に2と表示されます。
addNumber(1).then(function(number){
console.log(number)
})
// 2
この関数を先ほど動作確認したPromiseの連結方法を使って3つ繋げてみましょう。同じ関数をつなげただけですが3つをつなげているので3秒後に6が表示されます。前の処理の結果が次の処理に渡されていることがこの結果からもわかります。
addNumber(3).then(addNumber)
.then(addNumber)
.then(function(number){
console.log(number)
})
//6
上記の記述ではなく下記のように記述することも可能です。
addNumber(3).then(function(number){
addNumber(number).then(function(number){
addNumber(number).then(function(number){
console.log(number)
})
})
})
thenについて
Promiseを連結した場合にthenの中に再度Promiseの関数を入れていましたが、thenの中にPromiseではなくreturnで値を戻すことでthen を使って処理を連結することが可能です。
下記の例ではaddNumber(1)を実行してresolveで2が戻されます。次のthenに2を渡しますが次のthenの中では2*2してreturn します。returnした値は次のthenに渡され2*2*3が実行されます。最終的には48になります。
addNumber(1).then(function(number){
return number*2
}).then(function(number){
return number*3
}).then(function(number){
return number*4
}).then(function(number){
console.log(number)
})
//48
Promiseのresolveしかthenに渡せないわけではなくreturnを利用することで次のthen に値が渡せることも理解しておく必要があります。
最初のaddNumberをPromiseを使わずただのreturnを実行した場合は当然ながらエラーになります。then の中でreturnで次の処理に値を渡したい場合は、最初の関数はPromiseの関数である必要があります。
const addNumber = function(number){
return number+1;
}
addNumber(1).then(function(number){
return number*2
}).then(function(number){
return number*3
}).then(function(number){
return number*4
}).then(function(number){
console.log(number)
})
//エラー
複数のPromise処理の結果を一度に受け取る
複数のPromiseの処理の結果はPromise.allを利用することで一度に受け取ることができます。
Promise.all([addNumber(1),addNumber(2),addNumber(3)]).then(function(result){
console.log(result)
})
//[ 2, 3, 4 ]