メニューを閉じる

テクノデジタルグループ

メニューを開く

2015.12.01

プログラミング

JavascriptのPromiseを使おう

こんにちは、KTです。

社内勉強会で資料も作らずにPromiseについて語ったら、過去最大級に酷いグダグダ進行になってしまったので、リベンジも兼ねてざっとまとめたいと思います。

Promiseって?

ご存知のとおり、Javascriptは時間がかかる処理を非同期にすることが多いです。
Ajax、setTimeoutなんかがそうですね。Node.jsだとDBアクセスなんかも非同期だったりします。

非同期の処理は、同期処理に比べて書きづらく、特にJavascriptだと俗に言うコールバック地獄(後述します)に陥りがちです。

そんな非同期を簡単に扱え、かつ「この部分だけ同期で動かしたい」とかそんな要望にも簡単に応えることができる魔法のようなライブラリ、それがPromiseです。

 

その前に

同期・非同期って?

同期、非同期が何なの?って人のために簡単に説明すると、
Javascriptは、他の多くのプログラム言語と同様に、基本的には上から順にコードが実行され、実行したコードの処理が完了するまでは、待ち状態となります。これを同期処理(逐次処理)と呼びます。

例えば、以下のような例は同期処理ですね。

console.log('test 1');
for(var i = 0; i < 100; i++) {
// 時間がかかる処理
}
console.log('test 2');

この例では、‘test 1’が表示され、for文の長い処理が全て終わってから‘test 2’が表示されます。
このように同期処理では、時間がかかる処理を実行している間プログラムが完全に止まってしまうため、リアルタイム性が損なわれてしまう欠点があります。

その欠点を解消するため、時間がかかる処理を進めている間も他の処理を実行する非同期処理がJavascriptでよく使われています。

時間がかかる処理の例として、ajaxのコードを見てみます

console.log('test 1');
$.ajax({
    url: 'https://hogehoge.com',
      :
      :
    success: function(data) {
        console.log('Ajax Success');
    },
    error: function(req, err, errThrown) {
        console.log('Ajax Error');
    }
});
console.log('test 2');

ご覧のとおり、よくあるAjaxの例ですが、このコードを実際に実行すると‘test 1’が表示されてすぐ‘test 2’が表示され、その後Ajaxの結果によって‘Ajax Success’もしくは‘Ajax Error’が表示される、といったふうに動作します。
$.ajaxを呼び出してもプログラムが待ち状態にならずに次のコードを順次実行していくということですね。これが非同期処理です。

 

非同期が書きづらいってどういうこと?

このとおり、時間がかかる処理を動かす間も他の処理も進められる素敵な非同期処理ですが、
先ほどの例でもあったように、「時間がかかる処理に、『終わったらこれ呼んで』というメソッドを渡す」ことにより非同期処理を実現しています。
先ほどの例で言えば、「Ajaxが成功したらsuccess, 失敗したらerrorを呼んで」って感じですね。

他の例だと、setTimeout(○秒後に動作させる)なんかも

setTimeout(function() {
    console.log('1秒後の世界');
}, 1000);

のように、「○秒後にこれやって」というメソッドを渡して実現します。この、「○○したら呼び出すメソッド」をコールバックと呼びます。

ここまでは良いのですが、非同期処理を連続して行う場合、
例えば「API1にAjaxして、その結果をAPI2にAjaxして・・・」というような処理を実現する場合

$.ajax({
    url: 'API1',
    success: function(data) {
        console.log('API1が終わった');
        $.ajax({
            url: 'API2',
            data: data,
            success: funciton(data2) {
                console.log('API1もAPI2も終わった');
                $.ajax(...);
            }
        });
    }
});

のようにAPI1のAjaxのコールバック内でAPI2のAjaxを呼び出し、そのコールバック内で・・・というようにコールバックのネストが多くなってしまいます。これをコールバック地獄と言ったりします。

 

このように非同期処理をよく使うコードを書く場合に、コールバック地獄にならないように書くためのライブラリがPromiseです。

Promiseは、jQueryが提供しているので、Promiseを使う場合はjQueryを導入しましょう。(Ajaxを使う場合はおそらく導入済みだと思います。)
Node.jsの場合は、標準でサポートしていますし、Bluebirdというライブラリを使ってもよいでしょう。

 

Promiseの使い方

 非同期処理を変更する

Promiseを使うためには、まずは非同期処理をPromiseが扱える処理」に変更する必要があります。(Promisifyと言ったりします)

以降はPromisifyした非同期処理をPromiseメソッドと呼びます。

非同期処理をPromisifyするにしなければいけないことは以下の3点です。

  1.  jQuery.Deferred().promise()を返す
  2. 非同期の処理が成功で完了したらjQuery.Deferred().resolve()を呼び出す
  3. 非同期の処理が失敗したらjQuery.Deferred().reject()を呼び出す ※ 失敗しない場合は不要

例として「○秒後に処理を呼び出す」Promiseメソッドを実装してみましょう。

まず元々のコード(Promisifyする前)は以下のような感じです。

var mySetTimeout = function(callback, time) {
    setTimeout(callback, time);
};

まずはjQuery.Deferred().promise()を返しましょう。

var promisifiedSetTimeout = function(callback, time) {
    // jQuery.Deferredオブジェクトを生成
var d = $.Deferred();

    setTimeout(callback, time);
   // jQuery.Deferred().promise()を返却
    return d.promise();
};

続いて、成功した時にjQuery.Deferred().resolve()を呼び出しましょう。

var promisifiedSetTimeout = function(callback, time) {
    var d = $.Deferred();
    // タイムアウトした時にjQuery.Deferred().resolve()を呼び出す
    setTimeout(function() {
        callback();
        d.resolve();
    }, time);
    return d.promise();
};

setTimeoutでは失敗することは無いので、今回は 3. の手順は不要ですね。
これで非同期側の処理の変更は終わりです。

 

非同期処理を呼び出す

非同期処理をPromisify出来たら、次はそれを呼び出してみましょう。
下記のいずれかを組み合わせて書くことが多いです。

  1. 同期的に呼び出したい → then()
  2. 並列で呼び出したい → when()
  3. 成功時の処理を書きたい → done()
  4. エラー時の処理を書きたい → fail()
  5. 成功、エラーに関わらず終了時の処理を書きたい → always()

いくつか例を紹介します。

同期呼び出し

まずは全て同期的に呼び出してみましょう。
〜.then()で数珠つなぎにすればOKです。最後に〜.done()をつけると全て成功した場合にdoneが呼び出されます。
これで、methodAの処理が終わったらmethodB、その次にmethodCといったように、同期的に処理が行われます。

methodA()
.then(methodB)
.then(methodC)
.done(function() {
    console.log('\(^o^)/');
});

methodA()を実行した結果をmethodBで受け取りたい場合は、ちょっと面倒ですが以下のようにします。

methodA()
.then(function(data){

    return methodB(data);
})
.done(function() {

    console.log('\(^o^)/');
});

非同期呼び出し(並列呼び出し)

methodA〜Cまで並列で呼び出したい場合は$.when()に渡せばOKです。
これで、methodA〜Cが同時に実行され、全て完了した時に〜.done()が呼ばれます。

$.when(
    methodA(),
    methodB(),
    methodC()
).done(function(dataA, dataB, dataC) {
    console.log('\(^o^)/');
});

ここで、$.when()に渡すのは並列に動かすメソッドではなく並列に動かすメソッドの戻り値(jQuery.Deferred().promise()であることに注意してください。

// thenの場合
〜.then(methodA).then(methodB).done(〜)

// whenの場合
$.when(methodA(), methodB()).done(〜)

 

同期・非同期合わせ技

例えば、
methodAとmethodBを並列に呼び出して、両方の処理が終わったらmethodCを呼び出す
といったようなちょっと複雑な呼び出しをしてみましょう。

と言っても、上で述べたやり方をつなげるだけでOKです。

$.when(
    methodA(),
    methodB()
).then(methodC)
.done(function() {
    console.log('\(^o^)/');
});

methodA, BとCの呼び出し順を逆にしてみましょう。

methodC()
.then(function() {
    return $.when(
        methodA(),
        methodB()
    );
}).done(function() {
    console.log('\(^o^)/');
});

簡単ですね。

ちなみに、jQueryのAjaxは既にPromisifyされているので、自分でPromiseを返す必要はありません。

$.ajax(apiA).done(function〜

のように書くことが出来ます。超ラクチン!

 


【記事への感想募集中!】

記事への感想・ご意見がありましたら、ぜひフォームからご投稿ください!
  • こんな記事が読んでみたい、こんなことが知りたい、調べてほしい!という意見も募集中!
  • いただいた感想は今後の記事に活かしたいと思います!

感想フォームはこちら


【テクノデジタルではエンジニア/デザイナーを積極採用中です!】

下記項目に1つでも当てはまる方は是非、詳細ページへ!
  • 自分でアプリを作ってみたい
  • ITで世の中にワクワクを生み出したい
  • 使いやすさ、デザインにこだわったWebサイトを開発したい

採用情報の詳細はこちら


Qangaroo(カンガルー)

  • 徹底した見やすさと優れた操作性で、テストの「見える化」を実現。
  • テストの進捗が見える。開発がスマートに進む。
  • クラウド型テスト管理ツール『Qangaroo(カンガルー)』

【テクノデジタルのインフラサービス】

当社では、多数のサービスの開発実績を活かし、
アプリケーションのパフォーマンスを最大限に引き出すインフラ設計・構築を行います。
AWSなどへのクラウド移行、既存インフラの監視・運用保守も承りますので、ぜひご相談ください。
詳細は下記ページをご覧ください。

https://www.tcdigital.jp/infrastructure/

最近の記事