4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-31

対象とする読者

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が返るのねと一瞬でわかるからってことです。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?