対象とする読者
Promiseとasync/awaitをなんとなく理解している方。使ってみたけど挙動がよくわからなかったという方。
突然の仕変。同期から非同期に
こういう処理があったとして…
function hoge() {
// なんかかんか
var foo = ...
var bar = ...
if (foo < ...) {
return foo+bar;
}
var result = innerHoge(foo, bar);
var lastResult = ... // result使ってごにょごにょ
return lastResult;
}
function innerHoge(foo, bar) {
... // fooとbarをごにょごにょ
return ...;
}
あとから、「innerHogeはXHRで得たデータを元にしないと有効なリターンを返せなくなってしまった!仕様変更だ!」 ってなったら、成功時と失敗時のコールバックを足してましたよね。以下はjQueryのajaxを使った例です。
function innerHoge(foo, bar, successCallback, errorCallback) {
// xhrでもなんでもいいけど成功と失敗かえす非同期の例として。fooとbarのごにょごにょは向こう側でやることに…
$.ajax({
url: "...",
data: { foo: foo, bar: bar },
success: function(data, status, xhr){
successCallback(data);
},
error: function(xhr, status, ex){
errorCallback(ex);
}
});
}
非同期で呼び出すからinnerHoge()の戻り値はhoge()に返せない。ピンチですね。呼び出し元のhoge()で引数のためのコールバック関数を作らないとならないし、そのhogeを呼び出すところも…と、影響が多くて泣きそうになります。
コールバック引数の追加なんてさせてなるものか
これをまずはPromise型オブジェクトに任せてしまえば、コールバック引数のsuccessCallbackとerrorCallbackを追加せずに済みますね。Promiseのコンストラクタに渡す関数の第一引数が成功時のコールバック、第二引数が失敗時のコールバックとなり、then, catchに渡した関数に返されますからね。
function innerHoge(foo, bar) {
return new Promise((resolve, reject)=>{
// xhrでもなんでもいいけど成功と失敗かえす非同期の例として。fooとbarのごにょごにょは向こう側でやることに…
$.ajax({
url: "...",
data: { foo, bar },
success: function(data, status, xhr){
resolve(data);
},
error: function(xhr, status, ex){
reject(ex);
}
});
});
}
このように既存のコールバック系の関数はPromiseでラップすることで、個別の差異を共通のインターフェースにしてしまえます。慣れてくるとPromise返してくれて扱いやすいわーとなってきますよ。
ちなみにES6だと、{ foo: foo, bar: bar } なんていう同じ名前の代入は、 { foo, bar } と省略できます。
いやでも戻り値型、変わっとるやんけ
はい。それでもhoge()は困ったままですね。引数は増えずに済みましたがPromiseなんて返されてもどうすんの?…となります。そこでasync/awaitの出番。こう書きちゃえばOKです。
async function hoge() {
// なんかかんか
var foo = ...
var bar = ...
if (foo < ...) {
return foo+bar;
}
var result = await innerHoge(foo, bar);
var lastResult = ... // result使ってごにょごにょ
return lastResult;
}
あら簡単! functionの前にasyncを、innerHogeの呼び出しでawaitをつけるだけ!
どういうカラクリでしょう…。上記のhogeをPromiseを使って全く同じ挙動となるコードに翻訳すると次のようになるってことです。
function hoge() {
// なんかかんか
var foo = ...
var bar = ...
if (foo < ...) {
return Promise.resolve(foo+bar);
}
return innerHoge(foo, bar).then(function(result){
var lastResult = ... // result使ってごにょごにょ
return lastResult;
});
}
awaitを使った戻り値は、本来innerHogeが返すPromiseオブジェクトのresolve値になります。また、awaitを使った場所から、そのasync function内のブロックの終わりまで、その値が使えてreturnまでできるように見えますが、実際にはthenで渡したThenable関数内にまるごと移動したような形となり、その返り値がresolve値となります。1
また関数にasyncを付加した時点で、その関数が返すreturn値は必ずPromiseオブジェクトになります。そのため、return foo+barも、それをResolve値としたPromiseオブジェクトとなります。
async/awaitつけまくりましょう
さて、これで async function hoge() ができましたが、さらにhoge()を呼び出している元も全部awaitにしないとならんとです。ただしそうする以上、そいつもasync functionブロック内じゃないといけない。async functionにしだしたら、async/awaitは伝播していくことになります。毒(蜜?)を喰らわば皿まで!面倒を見るしか!
ま、普通に.then, .catchで伝播を止めるって手もありますけど。
ちなみにPromiseに対応したinnerHogeですが、$.ajaxがいただけないのでfetchに置き換えてしまいましょう。fetchはXHRや$.ajaxのPromise版2です。
function innerHoge(foo, bar) {
return fetch(`...?foo=${foo}&bar=${bar}`);
}
すげースッキリ。上記ではasync functionではないですが、Promiseを返す関数であればawaitができますよ〜。でも読み手にわかりやすくするためにasync/awaitを付加することをお奨めします。3
async function innerHoge(foo, bar) {
return await fetch(`...?foo=${foo}&bar=${bar}`);
}
ちなみにawaitを使えばtry ... catch構文で、Promiseオブジェクトのreject値を例外処理として書けるようになるので、これまたスッキリですよ。
(async function() {
var result;
try {
result = await hoge();
} catch(ex) {
// ex = hoge()が返したPromiseオブジェクトのcatchに渡された値
}
})();