2017.05.23

Promiseとasync/awaitを用いた非同期処理の実装

  Promiseとasync/awaitを用いたJavaScriptの非同期処理について記述していきたいと思います。JavaScriptでは外部サーバと情報をやり取りする際に非同期処理を行います。従来JavaScriptではコールバックを用いて非同期処理を行っていました。しかしこのコールバックというものは、なかなかの曲者で多くの問題を抱えています。一番の問題は連続で非同期処理を行う際です。例えば以下のようなケースです。
  1. salesforce組織にログイン認証を行う
  2. 取引先の住所を取得する
  3. 取引先の住所から、最寄りのレストランの情報を取得する
  4. 画像を読み込む
  5. 画像をリサイズし描画する

  上の①~⑤の処理は全て一つ一つ順番に処理をしなければならない非同期処理です。コールバックのみで記述しようとすると、上記のケースではどうしても処理全体をまとめてテストしなければならなくなります。 ES6、ES7で登場した新しいAPI、Promiseとasync/awaitを用いると非同期処理を一つ一つテストができるようになります。仕様変更によるテストの修正やバグの発見がスムーズになると思います。またコールバックのみの場合よりもネストを浅くできます。可読性や保守性の向上に繋がると考えています。
  以下は実際に連続した非同期処理をコールバックで記述した場合のサンプルソースになります。

ソース1 jQueryを使った非同期処理


// 以下一部処理を省略

var jsforce = require('jsforce');
var conn = new jsforce.Connection();

// 1.salesforce組織にログイン認証を行う
conn.login('userName','password',function(err, ret){
    // エラー処理
    if (err) {
        return console.error(err);
    }

    // 2.取引先の住所を取得する
    conn.query('SELECT BillingAddress FROM Account WHERE Name = "' + name + '"',function(err, resAddress) {
        // エラー処理
        if (err) {
            return console.error(err);
        }

        // 3.取引先の住所から、最寄りのレストランの情報を取得する
        $.ajax({
            data: { address:resAddress}   // 他のパラメータは略
        }).done(function(resData){
            // 読み込み中のマークを表示する
            nowLoading(true);

            // 画像のurlをresponseから取得
            var url = getUrl(resData);
            var img = new Image();
            img.src = url;

            // 4.画像を読み込む
            img.onload = function(){
                // 取得データからcanvasを作成
                var canvas = makeCanvas(resData);

                // 5.画像をリサイズし描画する
                resizeImg(canvas,function(){
                    // 読み込み中のマークを消す
                    nowLoading(false);
                });
            }

        }).fail(function(err){
            // エラー処理
            console.log(err);
        });

    });

});

  関数同士が密につながっています。非同期処理一つずつテストを行うことができず、関数全体で一度に行うことしかできません。これに対してPromiseとasync/awaitを使った場合どうなるのでしょうか。以下がそのソースです。

ソース2 Promiseとasync/awaitを使った非同期処理


var jsforce = require('jsforce');
var conn = new jsforce.Connection();

// 1.salesforce組織にログイン認証を行う
function login(){
    return new Promise(function(resolve,reject){
        conn.login('userName','password',function(err,ret){
            if(err){
                reject(err);
            }
            resolve();
        });

    });
}

// 2.取引先の住所を取得する
function getAddress(name){
    return new Promise(function(resolve,reject){
        conn.query('SELECT BillingAddress FROM Account WHERE Name = "' + name + '"',function(err,ret){
            if(err){
                reject(err);
            }
            resolve(ret);
        });
    });
}

// 3.取引先の住所から、最寄りのレストランの情報を取得する
function getRestaurantData(resAddress){
    return new Promise(function(resolve,reject){
        $.ajax({
            data: { address:resAddress}   // 他のパラメータは略
        })
        .done(function(ret){
            resove(ret);
        })
        .fail(function(err){
            reject(err)
        });
    }
}

// 4.画像を読み込む
function loadImage(resData){
    return new Promise(function(resolve,reject){
        // 画像のurlをresponseから取得
        var url = getUrl(resData);
        var img = new Image();
        img.src = url;

        // 画像を読み込む
        img.onload = function(){
            resolve();
        }
    });
}

// 5.画像をリサイズし描画する
function resizeAndDrawImage(resData){
    return new Promise(function(resolve,reject){
        // 取得データからcanvasを作成
        var canvas = makeCanvas(resData);

        // 画像をリサイズし描画する
        resizeImg(canvas,function(){
            resolve();
        });
    }
}

// 一連の処理を実行する
async function execAll(name){
    try{
        // 1.salesforce組織にログイン認証を行う
        await login();

        // 2.取引先の住所を取得する
        var resAddress = await getAddress(name);

        // 3.取引先の住所から、最寄りのレストランの情報を取得する
        var restaurantData = await getRestaurantData(resAddress);

        // 読み込み中のマークを表示する
        nowLoading(true);

        // 4.取引先の住所から、最寄りのレストランの情報を取得する
        await loadImage(restaurantData);

        // 5.画像をリサイズし描画する
        await resizeAndDrawImage(restaurantData);

        // 読み込み中のマークを消す
        nowLoading(false);

    } catch(err){
      // error処理
    }
}

// エントリポイント
execAll(name);

 コードの量は残念がら増えてしまいました。しかし非同期処理ごとに関数が記述されているため、1つ1つばらばらにテストができるようになりました。あまり見かけない記述の仕方がされていますので簡単に説明します。

// 2.取引先の住所を取得する
function getAddress(name){
    return new Promise(function(resolve,reject){
        conn.query('SELECT BillingAddress FROM Account WHERE Name = "' + name + '"',function(err,ret){
            if(err){
                reject(err);
            }
            resolve(ret);
        });
    });
}

 resolve()の引数として渡した値が戻り値になります。await getAddress(name);であればretが戻り値になります。またreject()が呼び出されるとその引数がcatchに渡されます。この場合はerrです。

// 一連の処理を実行する
async function execAll(name){
    try{
        // 1.salesforce組織にログイン認証を行う
        await login();

        // 2.取引先の住所を取得する
        var resAddress = await getAddress(name);

        // 3.取引先の住所から、最寄りのレストランの情報を取得する
        var restaurantData = await getRestaurantData(resAddress);

        // 読み込み中のマークを表示する
        nowLoading(true);

        // 4.取引先の住所から、最寄りのレストランの情報を取得する
        await loadImage(restaurantData);

        // 5.画像をリサイズし描画する
        await resizeAndDrawImage(restaurantData);

        // 読み込み中のマークを消す
        nowLoading(false);

    } catch(err){
      // error処理
    }
}

 非同期処理を記述した関数にはawait login()のようにawaitと関数の前に記述します。awaitを含む関数のfunctionの前にはasyncと記述する必要があります。

 今回はES6,7の新しいAPIを用いた非同期処理の方法を記述してみました。JavaScriptの機能は凄い速度で拡張されています。中には使い勝手の悪い機能や数年のうちに消えてしまう機能も存在します。しかし今回説明したPromiseとasync/awaitのように興味深い機能もたくさんあります。前述のBabelを使って色々と試してみるのも面白いのではないでしょうか。


※注意※
ソース2は現状、一部のブラウザでしか実行することができません。対応ブラウザを増やすことができるツール「Babel」を使って変換したコードを実際には使うことになります。

1 件
     
  • banner
  • banner

関連する記事