JavaScriptのPromiseって何?を解決したい
JavaScriptのPromiseをなんとなく使っているけど実は理解できていないかもという人やJavaScriptを始めたばかりでPromiseという単語は聞いたことはあるが何?という人を対象に一人でも多くの人にPromiseを理解してもらえるように身近な例とシンプルなコードを使ってPromiseの説明を行っていきます。
Promiseを説明する場合は同期や非同期といった言葉と一緒に解説されることが多くJavaScriptの入門者の人であれば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){
//ここに処理を記述する
})
// アロー関数を利用して下記のように記述することも可能です。
let yakusoku = new Promise((resolve, reject) => {
// ここに処理を記述する
});
最後に約束を守った場合と守らなかった場合のコードを追加します。約束を守った場合はresolveに値を入れ、守らなかった場合にはrejectに値を入れます。
const keep_promise = true
const yakusoku = new Promise(function(resolve,reject){
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
})
const keep_promise = true
const yakusoku = new Promise((resolve,reject) => {
if (keep_promise){
resolve('約束通りついたよ。')
} else {
reject('約束破ってごめん。')
}
})
これだけでPromiseで約束をコード化することは完了です。keep_promiseの値によりresolveまたはrejectの値が戻されます。
100%約束通りにカフェに到着すると言い切れる人なら下記のようにrejectを省略して、resolveのみを記述することも可能です。
const yakusoku = new Promise(function(resolve){
resolve('いつも約束通りつくよ。')
})
const yakusoku = new Promise((resolve) => {
resolve('いつも約束通りつくよ。')
})
またPromise.resolveを使ってnew Promiseを簡略化することも可能です。
const yakusoku = Promise.resolve('いつも約束通りつくよ。')
resolveとrejectを持つPromiseを関数を使って記述することもできます。
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)
})
// keep_promise = trueの場合
yakusoku.then((comment) => {
console.log(comment)
})
commentにはresolveの引数に記述した内容が入り、実行すると”約束通りついたよ。”が表示されます。
約束通りついたよ。
文字列ではなくresolveに入れる値が配列の場合はどのようになるかも確認しておきましょう。
if (keep_promise){
resolve(['a','b','c'])
} else {
reject('約束破ってごめん。')
}
結果は、配列が取得できます。
[ 'a', 'b', 'c' ]
Promiseを使う(約束を守らなかった場合)
約束を守らなかった場合(keep_promise = false)はどのようにrejectに入れた値を取得するのか確認します。
約束を守らなかった場合はthenではなくcatchを使うことでrejectの値を取得することができます。約束を守った場合はthenの中、約束を守らなかった場合はcatchの中と結果によって別々の処理を行うことができます。
// keep_promise = falseの場合
yakusoku.then(function(comment){
console.log(comment)
})
.catch(function(comment){
console.log(comment)
})
// keep_promise = falseの場合
yakusoku.then((comment) => {
console.log(comment)
})
.catch((comment) => {
console.log(comment)
})
実行するとcatchの中の処理が実行され、”約束破ってごめん。”が表示されます。
約束破ってごめん。
catchを利用することが一般的ですが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をよく見かける例としては外部のリソースからデータを取得するaxiosがあります。最初axiosを使った時は変わった記述方法だなと思った人もここまでの説明が理解できれば納得できるかと思います。axios.get(‘/api)の処理がこれまでのyakusoku関数に対応します。
axios.get('/api').then(function(response){
//データの取得に成功した場合に実行するコードを記述
}).catch(function(error){
//データの取得に失敗した場合に実行するコードを記述
});
axios.get('/api').then((response) => {
//データの取得に成功した場合に実行するコードを記述
}).catch((error) => {
//データの取得に失敗した場合に実行するコードを記述
});
ここまでの説明でPromiseを構成するresolve, reject, then, catchなどを理解することができました。
Promiseのステータスとは
Promiseは3つのステータスを持っています。pending, fulfilled, rejectedの3つです。ステータスの状態を視覚的にも理解するためにChromeのデベロッパーツールを利用します。
これまでに出てきたresolveとrejectとの関係を確認します。ステータスのfulfilledとrejectedとの関係は簡単でステータスfulfilledは約束を守ってresloveの値が戻された場合のステータスに関係し、スターテスrejectedは約束が守られずrejectの値が戻された場合のステータスに関係します。
ChromeのデベロッパーツールでresolveとrejectのPromiseのステータスを確認することができます。keep_promiseの値を変えることで両方のステータスを確認することができます。
const keep_promise = true;
const yakusoku = new Promise((resolve,reject) => {
if (keep_promise){
resolve('約束通りついたよ。');
} else {
reject('約束破ってごめん。');
}
});
console.log(yakusoku);
const keep_promise = false;
const yakusoku = new Promise((resolve,reject) => {
if (keep_promise){
resolve('約束通りついたよ。');
} else {
reject('約束破ってごめん。');
}
});
console.log(yakusoku);
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)
})
console.log(yakusoku)
//5秒を過ぎたら再度
console.log(yakusoku)
デベロッパーツールを見ると上記では5秒後にresolveかrejectが戻されるのでその間はpendingのステータスとなります。
keep_promiseをfalseに設定している場合には5秒経過するとコンソールにエラーが表示されます。その後ステータスを確認するとrejectedになってことが確認できます。
keep_promiseをtrueにしている場合にはUncaught ( in promise)は表示されません。
ここまでの動作確認でPromiseの3つのステータスの意味が理解できたのではないでしょうか。
Promiseをどのように使う
ここまで読み進めた人であれば、Promiseのresolve, reject, then, catch, ステータスのfulfilled, rejected, pendingの違いを理解することができているのではないでしょうか。ではPromiseはどんな場合に利用することができるのでまずは下記の例で確認します。
教室で名前順に生徒の名前を呼ぶ場合を考えます。
console.log('明石')
console.log('井上')
console.log('上田')
//コンソール
明石
井上
上田
上から順番に処理が行われるのでコンソールには名前は上から順番に表示されます。(これを同期処理と呼びます)
ここで井上を呼ぶ際に1秒間間を置くとします。setTimeout関数を利用して1秒経過に”井上”を表示するように変更します。
console.log('明石')
setTimeout(() => {
console.log('井上')
},1000)
console.log('上田')
//コンソール
明石
上田
井上
変更した結果、明石が呼ばれ、井上の前に上田が先に呼ばれ、井上がその後に呼ばれることになります。(これを非同期処理と呼びます)
ではsetTimeoutを利用して一定時間間を空けた後も順番通り名前を呼ぶためにはどうしたらいいでしょう。人によっては上田もsetTimeoutを設定すればいいと考える人もいるとは思います。
console.log('明石')
setTimeout(() => {
console.log('井上')
},1000)
setTimeout(() => {
console.log('上田')
},1000)
//コンソール
明石
井上
上田
ここではsetTimeoutを指定しているので経過時間を設定することができますが処理が完了する時間がわからない場合にはこの方法は利用することができません。確実に井上が呼ばれた後に上田の名前を呼びたいという場合に利用できるがPromiseです。Promiseのthenはresolveが実行された後に処理されるのでその特性を利用します。
console.log('明石')
new Promise((resolve) => {
setTimeout(() => {
console.log('井上')
resolve()
},1000)
}).then(() => {
console.log('上田')
})
//コンソール
明石
井上
上田
Promiseの内部でsetTimeoutを処理している最中はPromiseのステータスはPendingです。処理が完了(fulfilled)するとresolveによりthenの処理が実行されることになります。確実に井上の後に上田が呼ばれるようになりました。(非同期処理を同期処理のように扱えるようになりました)
もし井上を読んだ後に問題が発生した場合は上田の名前が呼ばれないこともあるかもしれません。その時にはrejectとcatchを利用することができます。
console.log('明石')
new Promise((resolve,reject) => {
setTimeout(() => {
console.log('井上')
reject('校内放送')
},1000)
}).then(() => {
console.log('上田')
}).catch((error) => {
console.log(error)
})
//コンソール
明石
井上
校内放送
このように途中に時間のかかる処理があった場合にPromiseを利用をすることで順番に処理を行うことができるようになります。
Promiseを戻すとは
外部リソースからデータを取得する際にfetch関数やaxiosライブラリが利用されます。その際に”Promiseを戻す”という言葉を聞くかもしれません。Promiseを戻すということはどういうことなのかfetch関数を利用して確認します。
無料で利用できるJSONPlaceHolderのURLに対してfetch関数を利用してアクセスを行います。ブラウザのコンソールに下記を記述します。
const response = fetch('https://jsonplaceholder.typicode.com/todos/1')
しばらくしてconsole.log(response)を実行します。しばらくが重要です。fetch関数はネットワークを経由してJSONPlaceHolderからデータを取得するため即座にはデータは取得できません。コンソールに表示される内容からresponseの値をPromiseであることがわかります。fetch関数が内部でresolve, rejectを処理を行っているので我々がresolve, rejectを使ったコードに触れる必要がありません。
Promiseが戻されたということはfulfilledの場合にはthenを利用することで戻された値を取得することができます。
fetch('https://jsonplaceholder.typicode.com/todos/1').then(response => {
console.log(response)
})
“Promiseを戻す”関数やライブラリを利用した場合にはthenを利用してその後の処理を行えるということがわかりました。fetch関数の例からコードはPromiseを意識した内容となっていますがPromiseをnew Promiseでresolve, rejectでコードを記述しなくてもデータを戻してくれる側で利用されることもわかりました。そのためアプリケーション内では各所でPromiseを活用されているがresolve, rejectを使ったコードの記述方法がわからない/忘れたので自分でPromiseを戻すコードを記述する場合には調べないとコードが記述できないという人も多いのではないでしょうか。
ここまで理解できればPromiseの理解はかなり深まっていると思います。別のPromiseの記事をネット上で読んでもらえればさらに理解は深まると思います。
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 ]