はじめに
この記事では、Promiseを見たこともきいたこともない、というヒトがPromiseを乱暴に解釈しまとめています。
「ここ違うよ!」という編集リクエストは歓迎しますが、必ずしも「正しい」説明は保証できません。
非同期関数ってそもそもなーに?
ざっくり言えば、その場では結果が出ない関数のことを指す。APIとかはだいたいコレだな。
たとえば、「3秒待つ」とか「ユーザーがクリックしたら起動する」とか、あるいは「どこそこと通信してそこのデータを取ってくる」とか。
ここで挙げている「今自分がいる場所の緯度経度をGPS経由で取得する」関数もその一つだけど、これなんかはGPSから位置情報が返ってくるまで「結果が出ない」だろ?そういうこった。
Promiseとは?
コードを見やすくするために非同期関数に噛ませる特殊な関数のこと。
別にPromiseを使わなくても非同期関数は動くし、Promiseそれ自体は非同期関数でもなんでもないというのがポイント。
具体的には「この処理が終わるのを待ってからあの関数を実行したい」という場合に用いるのだ。
Promiseを使わずに関数を順番に実行する例
基本的に、関数の中で関数を呼び出す、というやりかたとなる。
非同期関数を使わない場合
function Ichi_Ban_kansu(){
console.log("おどれら、");
Ni_Ban_kansu();
}
function Ni_Ban_kansu(){
console.log("ウチのシマで何さらしとんじゃ……");
}
Ichi_Ban_kansu();
非同期関数(今回は例としてsetTimeout()
)を使う場合
function Ichi_Ban_kansu(){
console.log("おどれら、");
setTimeout(function(){
Ni_Ban_kansu();
}, 1000);
}
function Ni_Ban_kansu(){
console.log("ウチのシマで何さらしとんじゃ……");
}
Ichi_Ban_kansu();
ふつうにやると、関数の中に関数を書くことになる。
これを繰り返せば当然、コードは段々畑のように入り組んでいき、読みづらくなる。
んで、その段々畑にしたままで読みづらくなってる状態のコードはコールバック地獄(?)と呼ばれ、弾圧される運命にある。
今回setTimeout
という非同期関数を使っている……のだが、ここで一つ例をお見せしよう。
function Ichi_Ban_kansu(value){
setTimeout(function(){
var name = '暗黒' + value;
return name;
}, 1000);
}
var ryouri = Ichi_Ban_kansu('おやき');
console.log(ryouri);
たとえば、setTimeoutを使いたいけれど、この部分だけ関数として分割したい&中で使った値を関数の外へと出力したい場合。
パッと思いつくのは上のように値をreturnで返すやり方だが……コレ、不正解である。こういうことをしても、「暗黒おやき」という文字列は出てこない。
理由は簡単、非同期関数の内側では、returnで値を返すことができないのである。
実践例:Promiseと非同期関数を書いてみる
// 関数の宣言と処理
function watchCurrent(){
return new Promise(function(resolve, reject){
/* この中に非同期関をかく! */
var watchID;
var options = {
'enableHighAccuracy': true, // 位置情報の精度を高く
'timeout': 20000, // 20秒でタイムアウト
'maximumAge': 300000 // 5分間有効
};
watchID = navigator.geolocation.watchPosition(
function(position){
// 「非同期関数が」成功したときの処理。
var result = {};
// 緯度
result.latitude = position.coords.latitude;
// 経度
result.longitude = position.coords.longitude;
// 値を返す(関数の外に出力する)ときには、returnではなくこのresolveを使う。
// 今回のように、外に出したい値が2つ以上ある場合は配列にしたりオブジェクトにしてまとめよう。
resolve(result);
},
function(error){
// 「非同期関数が」失敗したときの処理。
// 通信に失敗した、ユーザーに位置情報の提供を拒否された、
// 位置情報がいつまでたっても送信されてこないなどのイレギュラーが発生したとき、
// この2番目の関数が実行されるのだ。Promiseを使うときは、ここにrejectを噛ませてやろう。
// 値を返す(関数の外に出力する)とき、returnではなくrejectを使う。
// 「値を返す」という動作はresolveと同じ。
reject(error.message);
}, options);
/* 非同期関数ここまで! */
});
}
// 関数の呼び出し&実行
watchCurrent().then(function(seikou){
// 「Promiseが」成功したときの処理
console.log(seikou);
alert('緯度: ' + seikou.latitude + '\n経度: ' + seikou.latitude);
}).catch(function(sippai){
// 「Promiseが」失敗したときの処理
console.log(sippai);
alert('どうやら、僕たちは失敗したらしい。');
});
非同期関数を使いつつ、中で扱った値を外に出したい場合。
そんな時はreturnの代わりにresolveとrejectを使ってやればよい。
(なお、resolve&rejectはPromiseと一体化した関数なので、Promiseをつけずにresolveだけ使うなんてマネもできない。)
そもそもPromiseを使う最大の意義はナニかというと、コードを見やすくすることだ。
だから、やり方としてはこのように、非同期通信がからむ部分だけをひとつの関数にまとめて書くのがベターである。
注意すべき点として、非同期関数(今回はwatchPosition)とPromiseそれぞれに成功時、失敗時の処理があるということが挙げられる。(ないヤツもあるよ。setTimeoutとかaddEventListenerとか。)
watchPositionは上でも書いたとおり、通信に成功したときにfunction(position){ ~
、通信に失敗したときにfunction(error){ ~
の処理が実行される(ちなみに、失敗時は引数であるerrorにエラーメッセージが代入される。)。
じゃあPromiseはどうなのかというと、値がresolveで返されたか、rejectで返されたかで成否判定をする。
Promiseは成功時にthen
処理を、失敗時にcatch
処理を行うのだが、
早い話、resolveとrejectの位置を入れ替えて以下のようにしてしまえば、通信に成功したときにcatch
、失敗したときにthen
の処理を実行する、なんてことができる。
// 関数の宣言と処理
function watchCurrent(){
return new Promise(function(resolve, reject){
/* この中に非同期関をかく! */
var watchID;
var options = {
'enableHighAccuracy': true, // 位置情報の精度を高く
'timeout': 20000, // 20秒でタイムアウト
'maximumAge': 300000 // 5分間有効
};
watchID = navigator.geolocation.watchPosition(
function(position){
// 「非同期関数が」成功したときの処理。
var result = {};
// 緯度
result.latitude = position.coords.latitude;
// 経度
result.longitude = position.coords.longitude;
reject(result);
},
function(error){
// 「非同期関数が」失敗したときの処理。
// 通信に失敗した、ユーザーに位置情報の提供を拒否された、
// 位置情報がいつまでたっても送信されてこないなどのイレギュラーが発生したとき、
// この2番目の関数が実行されるのだ。Promiseを使うときは、ここにrejectを噛ませてやろう。
// error.messageの部分にはエラー時のメッセージが入っているが、resolveを使うとこのエラーメッセージは成功扱いとして吐き出される。
resolve(error.message);
}, options);
/* 非同期関数ここまで! */
});
}
// 関数の呼び出し&実行
watchCurrent().then(function(seikou){
// 「Promiseが」成功したときの処理
console.log(seikou);
alert('緯度: ' + seikou.latitude + '\n経度: ' + seikou.latitude);
}).catch(function(sippai){
// 「Promiseが」失敗したときの処理
console.log(sippai);
alert('どうやら、僕たちはまた失敗したらしい。');
});
ちなみに、メジャーな記事ではちょっと捻って以下のような書き方をしていることが多い。
なお、内容は一番上のやつとまったく同じなので、コピペして見比べてみるとよいかも。
// 関数の宣言と処理
var watchCurrent = function(){
return new Promise(resolve, reject) => {
/* この中に非同期関をかく! */
var watchID;
var options = {
'enableHighAccuracy': true, // 位置情報の精度を高く
'timeout': 20000, // 20秒でタイムアウト
'maximumAge': 300000 // 5分間有効
};
watchID = navigator.geolocation.watchPosition(
function(position){
// 「非同期関数が」成功したときの処理。
var result = {};
// 緯度
result.latitude = position.coords.latitude;
// 経度
result.longitude = position.coords.longitude;
// 値を返す(関数の外に出力する)ときには、returnではなくこのresolveを使う。
// 今回のように、外に出したい値が2つ以上ある場合は配列にしたりオブジェクトにしてまとめよう。
resolve(result);
},
function(error){
// 「非同期関数が」失敗したときの処理。
// 通信に失敗した、ユーザーに位置情報の提供を拒否された、
// 位置情報がいつまでたっても送信されてこないなどのイレギュラーが発生したとき、
// この2番目の関数が実行されるのだ。Promiseを使うときは、ここにrejectを噛ませてやろう。
// 値を返す(関数の外に出力する)とき、returnではなくrejectを使う。
// 「値を返す」という動作はresolveと同じ。
reject(error.message);
}, options);
/* 非同期関数ここまで! */
};
}
// 関数の呼び出し&実行
watchCurrent.then(function(seikou){
// 「Promiseが」成功したときの処理
console.log(seikou);
alert('緯度: ' + seikou.latitude + '\n経度: ' + seikou.latitude);
}).catch(function(sippai){
// 「Promiseが」失敗したときの処理
console.log(sippai);
alert('いい加減飽きてはくれないだろうか。');
});
複数のPromiseをリレーさせる(名前あり関数)
たとえば、関数Aで加工して出した値を関数Bでまた加工したい、という場合にはこのやり方が有効だ。
function TypeA(kazu){
return new Promise(function(resolve, reject){
// kazuに4を足す。kazuが数字でないならエラーを出す。
if( typeof kazu == 'number' ){
var result = Number(kazu) + 4;
resolve(result);
}else{
reject('これ、数字じゃないです。');
}
});
}
function TypeB(math){
return new Promise(function(resolve, reject){
// TypeAから渡されたmathの中身が7より大きい数字なら計算を続け、そうでないならエラーとしてrejectを出す。
if(math > 7){
console.log(math);
var result = Number(math) + 8;
resolve(result);
}else{
reject('mathが7以下なので計算をやめてしまいました。あーあ。');
}
});
}
var targetnum = 4;
TypeA(targetnum).then(TypeB).then(function(result){
// TypeAとTypeBの両方でエラーが出なかったらここの処理が行われる
console.log(result);
}).catch(function(message){
// 失敗時の処理。TypeAかTypeB、どちらかが失敗したらその時点でここの処理が行われる
console.log(message);
});
ちなみに、ここでポイント。
名前あり関数を連結するとき、TypeBの引数1が指定されていないのにお気づきだろうか。
引数はどうするのか、というと、実はコレ、指定できない。
もうすこし正確に言えば、TypeAでresolveした値がそのまま自動的に、TypeBの引数になる……引き継がれるのである。
というわけなので、TypeA、TypeBでresolveできる値は一つだけ。
仮に2つ以上ある場合、resolveの中身は最初の1つだけに格納される。2つ以上の値が必要?配列でなんとかしよう。
当たり前だが、返り値はきちんと返さないと引き継がれない。
面倒くさがってresolveを書きもらすと、TypeAで使ったresultを利用できないから注意だ。
複数のPromiseをリレーさせる(名前なし関数)
function TypeA(kazu){
return new Promise(function(resolve, reject){
// kazuに4を足す。kazuが数字でないならエラーを出す。
if( typeof kazu == 'number' ){
var result = Number(kazu) + 4;
resolve(result);
}else{
reject('これ、数字じゃないです。');
}
});
}
var targetnum = 4;
TypeA(targetnum).then(function(math){
// TypeAから渡されたmathの中身が7より大きい数字なら計算を続け、そうでないならエラーとしてrejectを出す
if(math > 7){
console.log(math);
var result = Number(math) + 8;
// then(function(){ ~~~ })の中なら、resolveではなくreturnで値を返してOK。逆にPromiseがないのでresolveはここでは使えない
return result;
}else{
// thenの中でエラーを出力するときはrejectでなくthrowを使う。
// throw '足しても8にならなかったよ…'; <= この書き方でもOKだが、 new Errorと一緒に使うのがベターらしい。
throw new Error('mathが7以下なので計算をやめてしまいました。あーあ。');
}
}).then(function(result){
// TypeAとそのあとに続くthenの両方でエラーが出なかったらここの処理が行われる
console.log(result);
}).catch(function(message){
// 失敗時の処理。TypeAかそのすぐ後のthen、どちらかが失敗したらその時点でここの処理が行われる
console.log(message);
});
上の方で「resolveとrejectはPromiseと一緒でなければ使えない」と書いたが、これがその代表格みたいなもん。
ここでは、resolveの代わりにreturn、rejectの代わりにthrowを使っている。
あ、そうそう。
setTimeout()
やアニメーション処理など、確実に成功することが分かっている処理に関しては、失敗時の処理を書かないというのもアリだろう。
function Three_counts(){
return new Promise(function(resolve){
setTimeout(function(){
resolve('3秒経ったよ!');
}, 3000);
});
}
Three_counts().then(function(text){
console.log(text);
return text;
}).then(function(text){
console.log(text + 'までに、3秒かかりました。');
});
Promise.all
2つ以上の関数を同時に処理し、両方の値が揃ってから計算をしたい場合は「Promise.all」という関数も使える。
function TypeA(kazu){
return new Promise(function(resolve, reject){
// 2秒経過したら4を足す。kazuが数字でなければエラーを返す。
setTimeout(function(){
if(typeof kazu == 'number'){
var result = kazu + 4;
resolve(result);
}else{
reject(kazu + '、数字じゃないです。');
}
}, 2000);
});
}
function TypeB(math){
return new Promise(function(resolve, reject){
// 5秒経過したら8を足す。mathが数字でなければエラーを返す。
setTimeout(function(){
if(typeof math == 'number'){
var result = math + 4;
resolve(result);
}else{
reject(math + '、数字じゃないです。');
}
}, 5000);
});
}
Promise.all([TypeA(4), TypeB(8)]).then(function(answer){
// answerは配列として返される。
console.log(answer);
// それぞれ別処理したいなら、このように書こう。
console.log(answer[0]);
console.log(answer[1]);
}).catch(function(error){
// どちらかがエラーになった時点でこのcatch処理に進むことになる。
// この性質上、errorに入るのは先に出たrejectだけ。
// たとえば両方エラーだった場合、先に処理が終わるTypeAのエラーメッセージがここに入るというわけだ。
console.log(error);
});
ちょっとしたおまけ:「async await」
Promiseよりさらに進化して使いやすくなったと噂のコード……async/awaitの比較も載せておこう。
以下の2種のコードはどちらも同じ効果を持つ。
Promiseのみ
function ThreeSecond(){
return new Promise(function(resolve){
setTimeout(function(){
resolve('3秒経過しました');
}, 3000);
});
}
function Result(){
ThreeSecond().then(function(answer){
console.log(answer);
});
}
Result();
async awaitも使ったバージョン
function ThreeSecond(){
return new Promise(function(resolve){
setTimeout(function(){
resolve('3秒経過しました');
}, 3000);
});
}
async function Result(){
var answer = await ThreeSecond();
console.log(answer);
}
Result();
async awaitをさらに応用
async function Result(){
function funcSuc(position){
var answer = {};
// 緯度
answer.latitude = position.coords.latitude;
// 経度
answer.longitude = position.coords.longitude;
console.log(answer);
}
// 冒頭に出てきたGPSの関数。こういう書き方をすると、このawaitがある行の処理が終わるまでconsole.log("end.")は実行されない。
// 成功時はfuncSucが実行される。
await navigator.geolocation.watchPosition(funcSuc);
console.log("end.");
}
Result();
async, awaitは、function ThreeSecond
にあたる部分はpromiseを利用したFunctionに対して使う必要はない。
REST API など、外部サービスが提供している非同期関数に対して利用することも可能だ。つまり、「すぐには答えが出ない処理」であるなら、このasync/awaitの出番というわけだ。
参考資料
https://noumenon-th.net/programming/2018/12/14/promise1/
https://rightcode.co.jp/blog/information-technology/javascript-async-await
https://www.wareko.jp/blog/concatenate-javascript-promise-in-multiple-stages
https://qiita.com/matsuby/items/3f635943f25e520b7c20
https://qiita.com/YoshikiNakamura/items/732ded26c85a7f771a27
https://qiita.com/kiyodori/items/da434d169755cbb20447
https://qiita.com/don-bu-rakko/items/b283829c4572a6425a5c
https://qiita.com/nekoneko-wanwan/items/f6979f687246ba089a35
https://memo.appri.me/programming/gsi-geocoding-api#%E9%80%86%E3%82%B8%E3%82%AA%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0API
https://qiita.com/YoshikiNakamura/items/732ded26c85a7f771a27#%E9%9D%9E%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86%E3%82%92%E8%A1%8C%E3%81%AA%E3%81%86%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%9C%E3%82%8Bpromise%E7%89%88
http://www.tohoho-web.com/ex/promise.html
https://qiita.com/toshihirock/items/e49b66f8685a8510bd76
https://www.sejuku.net/blog/52314
https://noumenon-th.net/programming/2018/12/14/promise1/
https://noumenon-th.net/programming/2018/12/14/async1/
https://webbibouroku.com/Blog/Article/promises
-
本来ならTypeA(targetnum)のように「TypeB( 引数 )」という書き方をするはずである。 ↩