はじめに
PromiseとはES2015で導入された機能(オブジェクト)で、Promiseを使う事で非同期処理の取り扱いが容易になります。
有名なところではaxiosもPromiseを使っていて、サーバとのHTTP通信をバックヤードで非同期で処理しています。このaxiosを含め、知らないうちに日常的にお世話になっているPromiseについて、もう一段理解を深めるため記事を作成しました。
Promiseを理解するためには、「非同期処理」と「コールバック」から理解する必要があると思いますので、そのあたりから始めたいと思います。
なおこの記事は味噌汁を作る過程を説明に引用しておりますが、あまり深い意味はありませんw
非同期処理とは
物事を順番にこなすことを同期処理と言います。例えば豆腐とネギの味噌汁を作る場合、
- 豆腐を切る(1分)
- ネギを切る(2分)
- お湯を沸かす(3分)
- 出汁をとる(2分)
- 豆腐とネギをお湯で煮る(3分)
- 味噌を溶かす(2分)
の工程があった時に、1→2→3→4→5→6と一つずつ順番に行うと、13分かかる計算になりますが、1,2と3,4を並行して行うと所要時間は10分で済みます。
この場合、豆腐とネギを切っている合間に行うお湯を沸かす工程が非同期処理になります。
コールバックとは
コールバックとは非同期処理の完了後に実行する関数で、お湯が沸いたあと出汁をとる部分がコールバックに該当します。
ここまでのところをコードで表現すると、以下のようになります。
boilWaterはお湯を沸かす関数、makeSoupは出汁をとるコールバック関数です。8行目で引数として渡したmakeSoupをboilWater関数内の処理完了後に呼び出しています。
const boilWater = (callback) => {
console.log('3.お湯を沸かす')
setTimeout(() => {callback()},1000)
}
const makeSoup = () => { console.log('4.煮干しで出汁をとる(コールバック)') }
boilWater(makeSoup)
console.log('1.豆腐を切る')
console.log('2.ネギを切る')
実行結果は以下の通りです。実行順序に注目してください。
3.お湯を沸かす
1.豆腐を切る
2.ネギを切る
4.煮干しで出汁をとる(コールバック)
Promiseで書き換えてみる
上記のコードはPromiseを用いて書き換えることが可能です。試しに書き換えてみます。
const boilWater = new Promise((resolve, reject) => {
console.log('3.お湯を沸かす')
setTimeout(() => {resolve()},1000)
})
const makeSoup = () => {
console.log('4.煮干しで出汁をとる(コールバック)')
}
boilWater.then(makeSoup)
console.log('1.豆腐を切る')
console.log('2.ネギを切る')
変更箇所は以下2点で、実行結果は同じになります。
-
boilWaterをPromiseインスタンスで定義している - コールバック(
makeSoup)をthenメソッドの引数として渡している
Promiseインスタンス内の引数であるresolve/rejectはそれぞれインスタンス内に記述した処理(console.log('3.お湯を沸かす'))が成功/失敗した場合のコールバックになります。
また.thenはコールバックを渡すためのメソッドで.thenを使う事で**Promiseチェーン(後述)**という記述ができるようになります。これはPromiseならではの仕様で、Promiseを使うメリットの1つです。
Rejectを使う
console.logは失敗しないので成功パターンしか記述していませんでしたが、コードを少し書き換えて失敗パターンを追加してみます。
失敗パターンとして、沸騰させていたお湯が途中で吹きこぼれるケースを考えます。お湯の温度が上がりすぎて吹きこぼしてしまった場合、吹きこぼしを雑巾で拭かないといけませんね。
cleanSpills(吹きこぼしを拭く)で非同期処理失敗時に呼び出されるコールバック関数を定義し、.catchメソッドを使ってPromiseに失敗時のコールバック関数を渡します。
const boilWater = new Promise((resolve, reject) => {
console.log('3.お湯を沸かす')
const rand = Math.random()
if(rand < 0.5){
// 処理成功
setTimeout(() => {resolve()},1000)
} else {
// 処理失敗
setTimeout(() => {reject()},1000)
}
})
const makeSoup = () => {
console.log('4.煮干しで出汁をとる(成功時のコールバック)')
}
const cleanSpills = () => {
console.log('吹きこぼしを掃除する(失敗時のコールバック)')
}
boilWater.then(makeSoup).catch(cleanSpills)
console.log('1.豆腐を切る')
console.log('2.ネギを切る')
///処理成功
3.お湯を沸かす
1.豆腐を切る
2.ネギを切る
4.煮干しで出汁をとる(成功時のコールバック)
///処理失敗
3.お湯を沸かす
1.豆腐を切る
2.ネギを切る
吹きこぼしを掃除する(失敗時のコールバック)
ここでcooking.jsの21行目に注目してください。
boilWater.then(makeSoup).catch(cleanSpills)
.thenと.catchを数珠つなぎに記述しています。このような書き方ができるのは、.thenは戻り値として新しいPromiseインスタンスを返すためです。同じ理由で.catchの後に.thenを続けることもできます。これらもPromiseならではの仕様と言えます。
Promiseチェーン
上記の通り.thenは戻り値として新しいPromiseを生成するため、数珠つなぎにすることで複数の非同期処理を順番に処理させることが可能です。これをPromiseチェーンと言います。
- 豆腐を切る(1分)
- ネギを切る(2分)
- お湯を沸かす(3分)
- 出汁をとる(2分)
- 豆腐とネギをお湯で煮る(3分)
- 味噌を溶かす(2分)
ここで1~6の処理をPromiseで定義し直すことで、以下のようなシナリオをPromiseチェーンで順序立てて実行することができます。
- 1,2と3を並列で実行する
- 両方の処理が完了したら、4→5→6の順番に処理を行う →Promiseチェーン(.then)
- 3~6のいずれかの処理の途中で失敗(吹きこぼし)したら、処理を止めて吹きこぼしを掃除する。 →Promiseチェーン(.catch)
// 3. お湯を沸かす
const boilWater = new Promise((resolve, reject) => {
console.log('3.お湯を沸かす')
const rand = Math.random()
if(rand < 0.9){
// 処理成功
setTimeout(() => { resolve() },1000)
} else {
// 処理失敗
setTimeout(() => {reject()},1000)
}
})
// 1. 豆腐を切る + 2. ネギを切る
const cutFoods = new Promise((resolve, reject) => {
console.log('1.豆腐を切る')
console.log('2.ネギを切る')
setTimeout(() => {resolve()} ,1000)
})
// 4. 出汁をとる
const makeSoup = () => {
return new Promise((resolve, reject) => {
console.log('4.煮干しで出汁をとる')
const rand = Math.random()
if(rand < 0.9){
// 処理成功
setTimeout(() => { resolve() },1000)
} else {
// 処理失敗
setTimeout(() => {reject()},1000)
}
})
}
// 5. 豆腐とネギをお湯で煮る
const boilFoods = () => {
return new Promise((resolve, reject) => {
console.log('5.豆腐とネギをお湯で煮る')
const rand = Math.random()
if(rand < 0.9){
// 処理成功
setTimeout(() => { resolve() },1000)
} else {
// 処理失敗
setTimeout(() => {reject()},1000)
}
})
}
// 6.味噌を溶かす
const dissolveMiso = () => {
return new Promise((resolve, reject) => {
console.log('6.味噌を溶かす')
const rand = Math.random()
if(rand < 0.9){
// 処理成功
setTimeout(() => { resolve() },1000)
} else {
// 処理失敗
setTimeout(() => {reject()},1000)
}
})
}
// 吹きこぼしを掃除する(失敗時のコールバック)
const cleanSpills = () => {
console.log('吹きこぼしを掃除する(失敗時のコールバック)')
}
// Promiseチェーン
Promise.all([cutFoods,boilWater]).then(() => { return makeSoup() }).then(() => { return boilFoods() }).then(() => { return dissolveMiso() }).catch(cleanSpills)
3.お湯を沸かす
1.豆腐を切る
2.ネギを切る
4.煮干しで出汁をとる
5.豆腐とネギをお湯で煮る
6.味噌を溶かす
cooking.jsの最終行について補足します。
Promise.all([cutFoods,boilWater]).then(() => { return makeSoup() }).then(() => { return boilFoods() }).then(() => { return dissolveMiso() }).catch(cleanSpills)
冒頭のPromise.all()はPromiseの静的メソッドで引数で渡した配列内のPromiseの完了を待ちます。複数のPromise完了を待ち受ける時に便利です。両方の処理が成功した場合は.then、いずれかの処理が失敗した場合には.catchが呼び出されます。
ここでは1,2と3を並行で処理して、両方が完了したら次の処理(4)へ移行という部分を担っています。
また.thenのコールバックにPromiseをreturnする関数を無名関数で渡すことで、それぞれ個別に定義したPromise同士を繋げて処理を作ることができます。
以上