JavaScript
promise
async
es6
await

Promiseへの翻訳コードでasync/awaitの挙動を理解する


対象とする読者

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に渡された値
}
})();





  1. 意味合い的に同等のコードを示しているだけで具体的な実装はjavascriptエンジンに依るところです。実際にこんなコードが吐かれるわけではありません。 



  2. と思って差し支えはないでしょう。厳密にはいろいろ違います。 



  3. コードの中身を見なくても、async function→Promiseが返るのねと一瞬でわかるからってことです。