概要
JavaScriptで同期/非同期処理をやるときに避けられない、Promiseオブジェクトについて書いておきます。
書く目的は、自分がまた忘れてしまったときに振り返るためと、私と同じくらいのレベルの方の理解の補助になれば。(いろいろな書き方や資料がある中で、自分に刺さる例ってあると思うので)
なお(x) => x*2はfunction(x){return x*2;}の省略形です。自然に使える前提で書いてます。
参考
Promiseオブジェクトの理解
step1:遅い計算の実行
Promiseオブジェクトは、その中に関数を書いて定義すると、"そのうち"やってくれるもの。
例えば、
let p1 = new Promise((resolve, reject) =>
{
resolve(1+2)
})
こうやって書くと、1+2の処理を実行してくれます。resolve関数に引数の結果を詰めて呼ぶと、p1の中に格納されます。(1+2が遅い計算だとしてください。)
呼び出した側は、Promiseオブジェクトがnewされて、それがp1に格納されるところまでは、するっと行きます。p1に代入した後に別の処理があればどんどん進みます。でも1+2はまだ計算されません。1+2の部分がすごく遅い場合、例えばHTTPアクセスして情報を取得するとかの場合でも、同じです。つまりこの部分が非同期で動くということ。
→→メインの処理→→→→→→→・・・
└ Promiseの中の処理"1+2"が、メインと非同期で動く
step2:結果の利用
次にこの1+2の結果をどう扱うかというと、こんな感じ。
let p1 = new Promise((resolve, reject) =>
{
resolve(1+2)
}).then((data) => { // 追加
console.log(data) // 追加
}) // 追加
// 3
.then()を追加して、その中に関数を書いてます。引数のdataに、resolve()に渡した引数、1+2の結果である3が格納されています。その3を使って、次の処理であるconsole.log()をしています。
→→メインの処理→→→→→→→・・・
└ "1+2"の計算処理 → then()の中の処理
step3:さらにその次もやりたい!
2つだけでなく、どんどん続けたくなると思います。その場合はこうします。
let p1 = new Promise((resolve, reject) =>
{
resolve(1+2)
}).then((data) => {
console.log(data)
return new Promise((resolve, reject) => // 追加
{ // 追加
resolve(data+4) // 追加
}) // 追加
}).then((data) => { // 追加
console.log(data) // 追加
}) // 追加
// 7
step2で追加したthen()の中で、別のPromiseオブジェクトを作ってそれをreturnします。そのPromiseのでresolve()に値を入れてやると、その次のthen()で使えます。
→→メインの処理→→→→→→→・・・
└ "1+2"の計算処理 → then()の中で"3+4" → 2つ目のthen()の中の処理でdataは7
step1~3のまとめ
非同期処理を想定した1+2ですが、そのあと順番にthen()→then()となって、結局同期処理なんじゃないか?と私は混乱しました。そうなんです。非同期処理を、順番に起動してます。非同期処理を同期的に実行していると言っていいのかな。私はそういうイメージです。Promiseとthenを使うことで、例えば、最初のPromiseでHTTPアクセスのようにすべて取得し終えるのを待ち終わったらコールバック関数でresolve()に結果を詰めれば、その次のthenで、HTTPアクセスの結果を利用できます。
なんか、、これ↑、わかってる人にはわかるけど、わからない人にはわからないという文章になってそうな気がするので、次にちゃんと時間がかかる簡単な例を書きます。
f(x)=2*x+5の実装例(ただし2*xは3秒かかるとする)
3つの関数を定義して、それを呼び出す例です。
2倍する関数の実装
まずこの関数を定義します。3秒かかるようにしてあります。
function calc_double(x) {
return new Promise((resolve, reject) =>
{
try {
setTimeout(() => {
resolve(x*2)
}, 3000)
} catch (err) {
reject(err)
}
})
}
setTimeoutは、最初の引数の処理を、2つ目の引数のミリ秒後に実行します。なので、3秒後にx*2を実行して、その結果を引数にresolveを実行します。これで、3秒かかる2*xの関数が完成。
setTimeoutは、非同期処理の説明ですごくよく出てくる関数なので、動きがわからなければこれを機会に簡単な例を自分で作って完全理解することをお勧めします。システムとかの実装では頻繁には使わないかもだけど。
最初のstep1~3ではあえて説明しなかったrejectですが、resolveの代わりにrejectを呼ぶとエラーで止まるという仕組みです。引数にエラーの情報を入れると吉。
+5する関数の実装
今度は待たずにx+5しているだけなので単純です。Promiseやtry,catchしてるのが煩わしいですが。
function calc_plus5(x) {
return new Promise((resolve, reject) =>
{
try {
resolve(x+5)
} catch (err) {
reject(err)
}
})
}
f(x)=2*x+5の実装
最後の関数で、calc_doubleを呼び出し、そのあとthenでvalueをcalc_plus5に入れてます。
function calc_2x_plus5(x) {
return calc_double(x) // (1)
.then(value => calc_plus5(value)) // (2)
}
(1)の部分は、calc_doubleの定義から(new Promise((resolve,reject)=>resolve(x*2)))だな、つまりx*2を計算するPromiseが返ってくると見ます。
(2)の部分は、x*2を計算したPromiseのthenなので、valueはx*2の結果が入っていて、それをcalc_plus5に渡すと。そしてcalc_plus5もnew Promiseをreturnしてます。
結局全体のreturnとしては、calc_plus5のPromiseオブジェクトが返る関数になります。
関数を定義したことでちょっとわかりづらかったかもしれませんが、処理を境目を理解するにはその方がよいと思っています。また実際に使う場面でも、やっていることの理解がしやすいと思います。まずは2倍、つぎに+5かーといった具合に。
使い方
普通の関数のようにはいきません。使うときも、thenで結果を取り出します。3を入れれば11、5を入れれば15が表示されます。
calc_2x_plus5(3).then(v => console.log(v))
// 11
calc_2x_plus5(5).then(v => console.log(v))
// 15
ちなみに2*xに3秒かかる処理なのですが、1行目のcalc_2x_plus5(3)が流れた直後に2行目のcalc_2x_plus5(5)も流れます。つまりこんな感じ。
→→メインの処理→→→→→→→・・・
│└2*5で3秒待ち→→→10+5=15
└2*3で3秒待ち→→→6+5=11
2*3を計算し始めた直後に2*5を計算し始めます。ほぼ同時。なので3秒後に結果が出るのもほぼ同時です。6秒後ではありません。
実践の話
メインの処理とPromiseで世界線が分かれてしまったので、もう二度と出会うことがないかというとそうではなく、Promiseの世界線から、メインの変数にアクセスできます。またJavaScriptの場合だと、HTML上で何かのアクションをすることもあると思います。実際にはそうやって使います。
まとめ
Promiseの初歩は、なかなか敷居の高い1歩です。私自身、1歩上ったような、上らないような感覚ですが、いくつか実装したりいろんな説明を見たりして身に着けていきましょう。
触れなかった話として、Promiseコンストラクタに関数を入れて、その引数が2つあって、それに値を詰めて呼び出す、というところがあります。普通のプログラミングのように上から順に流れる処理からすると、この部分が直感的にすごくわかりづらい!なのであえて触れませんでした。とりあえずおまじないだと思っておけばよいのではないでしょうか。動きやなすべきことがわかってきたら、ちゃんとした資料やサイトを読みましょう。