6. 非同期処理

この章では、非同期処理の実装や制御に関連する方法を説明する。

6.1. Deferredによる非同期処理制御

6.1.1. 概要

Deferredは、複数のコールバックを柔軟に扱うことができるユーティリティー機能である。Deferredを利用することで、非同期処理実装時に陥りがちな、コールバックが多段階でネストする状態を避け、ソースコードの可読性・保守性を保つことができる。
deferred-nested-callback

図: コールバックのネスト例

利用ライブラリ サンプル 参考ページ
jQuery - Deferred Object | jQuery

6.1.2. Deferredのメリット

Deferredを利用すると、非同期処理をネストすることなく実装できる。
ここでは、非同期処理のネストをDeferredを利用して解消する方法を紹介する。
まず、Deferredを利用せずに非同期処理を実装したサンプルを示す。以下は1秒毎にメッセージを3つ出力する。
サンプル
1秒毎にメッセージを出力
setTimeout(function () {
  $('#deferred-area').append('<p>1</p>');
  setTimeout(function () {
    $('#deferred-area').append('<p>2</p>');
    setTimeout(function () {
      $('#deferred-area').append('<p>3</p>');
    }, 1000);
  }, 1000);
}, 1000);
次に、上記のサンプルにDeferredを適用し、ネストを解消する。
サンプル
1秒毎にメッセージを出力(Deferred適用)
// (1)
var outputMessage1 = function () {

  // (2)
  var dfd = new $.Deferred();

  setTimeout(function () {
    $('#deferred-area').append('<p>1</p>');

    // (3)
    dfd.resolve();
  }, 1000);

  // (4)
  return dfd.promise();
};

// (5)
var outputMessage2 = function () {
  /* omitted */
};

// (5)
var outputMessage3 = function () {
  /* omitted */
};

// (6)
outputMessage1()
.then(outputMessage2)
.then(outputMessage3);
項番 説明
(1)
1秒後にメッセージ「1」を出力する関数。
(2)
deferredオブジェクトを生成する。
(3)
非同期処理内で最後にdeferredオブジェクトの状態を変更する。
deferredオブジェクトの状態については後述する。
(4)
promiseオブジェクトを返却する。
(5)
メッセージ「2」「3」を出力する関数。
outputMessage1とほぼ同一であるため、実装は省略する。
(6)
outputMessage1を実行し、thenで後続の非同期処理を連結する。
thenについては後述する。
Deferred適用前後を比較すると、前者はメッセージ数分ネストしているが、後者はネストが解消している。Deferred特有の実装が必要だが、ネストを浅く保つことができる。

Note

thenを実行すると、promiseオブジェクトにコールバックを設定し、新たなpromiseオブジェクトが返却される。thenを繰り返すとpromiseオブジェクトが連鎖し、コールバックが直列的に実行される。

deferred-then

図: promiseオブジェクトの連鎖とコールバック

Note

promiseオブジェクトは、deferredオブジェクトから状態を変更するメソッドを削除したサブセットである。状態を変更できる箇所を限定することで、意図せず後続処理が実行されることを抑止する。

6.1.3. Deferredの利用方法

6.1.3.1. Deferredによるコールバックの切り替え(then)

Deferredのメリット では、thenで非同期処理を連結した。thenは非同期処理の連結だけでなく、deferredオブジェクトの状態に応じてコールバックを切り替えることもできる。
サンプル
thenによるコールバックの切り替え
// (1)
var random = function () {
  return Math.floor(Math.random() * 2) === 1 ? true : false;
};

var async = function () {

  var dfd = new $.Deferred();

  setTimeout(function () {
    var result = random();
    $('#deferred-area').append('<p>Result is ' + result + '.</p>');
    if (result) {

      // (2)
      dfd.resolve();
    } else {

      // (2)
      dfd.reject();
    }
  }, 1000);

  return dfd.promise();
};

// (3)
var success = function () {
  $('#deferred-area').append('<p>success</p>');
};

// (3)
var failure = function () {
  $('#deferred-area').append('<p>failure</p>');
};

// (4)
async().then(success, failure);
項番 説明
(1)
trueかfalseをランダムに返却する関数。
(2)
ランダム関数の結果に応じてdeferredオブジェクトの状態を変更する。
(3)
メッセージを出力する関数。
(4)
引数にコールバックを渡す。
thenに設定したコールバックは、deferredオブジェクトの状態に応じて実行される。対応関係を以下に示す。
メソッド 状態 実行するコールバック
resolve
resolved
第1引数
reject
rejected
第2引数
なお、resolvedに対応するコールバックが不要の場合はnullを設定すればよい。

6.1.3.2. Deferredによるコールバックの切り替え(done・fail)

ここでは、thenと同様にdonefailでコールバックを切り替える方法を紹介する。
サンプル
done・failによるコールバックの切り替え
JavaScriptは Deferredによるコールバックの切り替え(then) との差分のみ記述する。
// (1)
async()
.done(success)
.fail(failure);
項番 説明
(1)
引数にコールバックを渡す。
deferredオブジェクトの状態と、実行するコールバックの対応関係を以下に示す。
メソッド 状態 実行するコールバック
resolve
resolved
doneの引数
reject
rejected
failの引数

Note

donefailを実行すると、promiseオブジェクトにコールバックを設定し、同一のpromiseオブジェクトが返却される。donefailを繰り返すと1つのpromiseオブジェクトに複数のコールバックが積み上げられ、ほぼ同時に実行される。thenと挙動が異なる点に注意すること。

deferred-done

図: promiseオブジェクトとコールバック

Note

thendonefailの他にalwaysがある。alwaysはresolved・rejectedのどちらの状態でも実行される。

6.1.3.3. 非同期処理の並列連結(when)

ここでは、非同期処理を並列的に連結する方法を紹介する。whenは、複数の非同期処理を並列に実行し、結果に応じてコールバックを切り替えることができる。
サンプル
非同期処理の並列連結
JavaScriptは、非同期処理を3つ用意し、whenの引数に設定する。
// (1)
var asyncFuncA = function () {

  var dfd = new $.Deferred();

  setTimeout(function () {
    $('#deferred-area').append('<p>functionA has ended.</p>');
    dfd.resolve();
  }, 1000);
  return dfd.promise();
};

// (2)
var asyncFuncB = function () {
  /* omitted */
};

// (2)
var asyncFuncC = function () {
  /* omitted */
};

// (3)
var outputMessage = function () {
  $('#deferred-area').append('<p>All of the function has ended.</p>');
};

// (4)
$.when(asyncFuncA(), asyncFuncB(), asyncFuncC())
.then(outputMessage);
項番 説明
(1)
非同期処理終了後にメッセージを出力する関数。
(2)
非同期処理終了後にメッセージを出力する関数。
asyncFuncAと同様であるため、実装は省略。
(3)
全ての処理が完了後にメッセージを出力する関数。
(4)
非同期処理からpromiseオブジェクトを受け取る。
thenの引数にメッセージ出力処理を設定する。
上記の通り実装すると、各非同期処理の完了を待ってthenのコールバックが実行される。

Note

whenに複数の非同期処理を渡すと、pendingの状態を持つpromiseオブジェクトが返却される。 promiseオブジェクトは各非同期処理の状態を管理しており、全ての非同期処理がresolvedになるか、1つでもrejectedになるとthenのコールバックを実行する。

なお、後者の場合は実行中の非同期処理の完了を待ったり、中断することなくコールバックの実行に移る。必要に応じて中断処理などを実装すること。

6.1.4. 応用方法

6.1.4.1. Ajaxの再利用

ajaxは、非同期通信完了後にコールバックを実行できる。コールバックに業務処理を設定した場合、それらが密に結合される。Deferredを利用すると、非同期通信とコールバックの結合を疎にし、非同期通信のみ再利用することができる。
サンプル
Ajaxの再利用
var doAjax = function () {

  var dfd = new $.Deferred();

  // (1)
  $.ajax({
    url : 'js/deferred-promise-sleep.js',
    dataType : 'script'

  // (2)
  }).then(dfd.resolve);
  return dfd.promise();
};

// (3)
var funcA = function () {
  $('#deferred-area').append('<p>Do function A.</p>');
};

// (3)
var funcB = function () {
  $('#deferred-area').append('<p>Do function B.</p>');
};

// (4)
doAjax().then(funcA);

// (4)
doAjax().then(funcB);
項番 説明
(1)
非同期通信を実行する。
(2)
非同期処理終了後に状態を変更する。
(3)
メッセージを出力する関数。
(4)
非同期通信を実行し、thenの引数にメッセージを出力する関数を設定する。
上記のサンプルを実行すると、非同期通信は同一だが、異なるメッセージを出力することができる。AjaxとDeferredを組み合わせることで、再利用性を高めることができる。

Note

サンプルでは、ajaxthenを実装している。 ajaxは、jQuery XMLHttpRequest(以降、「jqXHR」とする。)を返却する。jqXHRはpromiseオブジェクトのインターフェースを実装しており、サンプルのようにthendonefailを実行することが可能である。詳細は jQuery公式ウェブサイトのリファレンスを参照すること。