[jQuery] 最も基本的なDeferred Objectの使い方

概要

jQueryのDeferredは非同期処理の実行順序を管理する機能。今回はその最も基本的な使い方を解説する。

解説

コールバックを登録する

deferred objectは次のように宣言することで使用可能となる。

const d = $.Deferred();

then関数にコールバックを指定することで処理を登録する。

const d = $.Deferred();
const d2 = d.then(() => {
  console.log('log1');
});

しかし、このままでは登録したコールバックは実行されない。なぜならdがpending(待機状態)となっているからだ。deferred objectの状態はstate関数で調べることができる。

console.log(d.state()); // -> pending

dの状態を変えることで次の処理に移行する。今回はresolvedにする。

const d = $.Deferred();
const d2 = d.then(() => {
  console.log('log1');
});
d.resolve(); // -> log1

ちなみに、thenの戻り値d2はpromise objectとなっている。promise objectはdeferred objectと殆ど同じ構造だが、stateを変更するresolveなどの関数が削除されているものである。途中の状態を強制的に書き換ることができないような作りになっているのだ。

複数の非同期処理を登録する

deferredオブジェクトには複数の処理を直列に繋げることもできる。

const d = $.Deferred();
const d2 = d.then(() => {
  console.log('log1');
});
const d3 = d2.then(() => {
  console.log('log2');
});
d.resolve();

この例では、dを実行するとd2のコールバックが実行されるが、そのコールバック実行後にd2も自動でresolvedとなる仕様なので、続けてd3のコールバックが実行される。

出力:
log1
log2

ではd2のコールバックが非同期処理だった場合はどうなるだろうか?

const d = $.Deferred();
const d2 = d.then(() => {
  setTimeout(() => {
    console.log('log1');
  }, 1000); // 1秒後に出力
});
const d3 = d2.then(() => {
  console.log('log2');
});
d.resolve();

この場合、コールバック自体はd2, d3の順で実行されるが、コンソールログは当然ながらlog2→log1の順になる。
ここからがdeferred objectの本領になる。deferred objectを正しく使えばこのような非同期処理であっても、d2の非同期部分の実行を待ってからd3を実行することができるのだ。

最初の例ではdがpendingとなっていたため、後に登録した処理が実行されなかった。この仕組みを利用する。
then関数の動作仕様は、中のコールバックが返す戻り値によって変化する。例えばd2のコールバックが何も返さない場合、d2が即resolvedとなるというのは先述の通りである。
しかし、コールバックがまた別のdeferred objectを返す場合、d2はそのdeferred objectと同じ状態になるという仕様がある。
具体的な例は以下で説明する。

const d = $.Deferred();
const d2 = d.then(() => {
  const inner_d = $.Deferred();
  setTimeout(() => {
    console.log('log1');
    inner_d.resolve();
  }, 1000); // 1秒後に出力
  return inner_d; // deferred objectを返す
});
const d3 = d2.then(() => {
  console.log('log2');
});
d.resolve();

この例ではd2のコールバック内で更に新しいdeferred object "inner_d"を作り、コールバックの戻り値としている。また、inner_dは非同期処理の実行後にresolveしている。つまり、非同期処理の実行後までpendingとなっているのだ。
このときの流れを追ってみよう。まずdがresolveされることで、d2のコールバックが呼び出される。d2のコールバックはinner_dを返すので、d3は非同期処理内のinner_dがpendingである限り呼び出されない。1秒後に非同期処理内のinner_d.resolve();が実行されることでd2がresolvedとなる。こうしてようやくd3が実行される。
非同期処理を待ってlog1→log2の順で出力することができた。

thenの戻り値は不要であれば変数に格納せず、thenを繋げて書くことができる。また、deferred objectは先にresolveしてから後にthenを繋げることもできる。以下のような書き方が一般的。

const d = $.Deferred().resolve();
d.then(() => {
  const inner_d = $.Deferred();
  setTimeout(() => {
    console.log('log1');
    inner_d.resolve();
  }, 1000); // 1秒後に出力
  return inner_d; // deferred objectを返す
}).then(() => {
  console.log('log2');
});

非同期処理は今回のように2つだけでなくfor文などを使って好きなだけ並べることもできる。

let d = $.Deferred().resolve();
for(let i = 0; i < 5; i++) {
  d = d.then(() => {
    const inner_d = $.Deferred();
    setTimeout(() => {
      console.log('log' + i);
      inner_d.resolve();
    }, 1000 - i*200); // 1秒後, 0.8秒後, 0.6秒後, 0.4秒後, 0.2秒後に出力
    return inner_d;
  });
}

Ajaxを利用する

jQueryにはAjaxというjs内で別のURLにアクセスする機能が備わっている。jQueryでのAjaxの実装はdeferred object (promise object)を利用した実装となっている。
jQueryのajaxの使い方は例えば以下のようになる。

$.ajax({
  url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js',
  type: 'get',
});

これだけではレスポンスを利用できない。
ここでthenを使用する。ajaxの戻り値はpromise objectであり、通信が完了するかタイムエラーになるまでpendingの状態となっている。したがって、このpromise objectにthenを繋げることで通信完了後の処理を定義することができる。レスポンスはthenのコールバックの引数として受け取ることができる。

$.ajax({
  url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js',
  type: 'get',
}).then((response) => {
  console.log(response);
});

thenのコールバックの中で再びajaxを呼び出すという処理も可能。

$.ajax({
  url: '(URL省略)',
  type: 'get',
}).then((response) => {
  // 何らかの処理・分岐など
  return $.ajax({
    url: '(URL省略)',
    type: 'get',
  });
}).then((response) => {
  // 何らかの処理
});

終わりに

今回はdeferred objectの最も基本的な部分のみを解説した。
deferred objectのstateにはpending, resolved以外にもrejectedという値がある。また、それに応じてthen以外にdone, fail, alwaysなどを使い分けることで、細かい条件分岐を実現することができる。他にもwhenなどの便利な機能も存在する。気になる場合は調べてみると良いだろう。