jQuery deferredの使い方 前編 deferredの基本

jQuery deferredをなんとなく知っているが使い方がよくわからない人のために短期シリーズでおさらいをします。第1回目は非同期処理の概念からdeferredの基本機能までをおさえます。

発行

著者 高津戸 壮 テクニカルディレクター
jQuery deferredの使い方 シリーズの記事一覧

なぜ、今、jQuery deferred?

この短期シリーズでは、2回に分けてjQuery deferredについて解説します。

jQuery deferredは、jQuery1.5で追加された機能であり、特に新しいものではありません。

しかしながら、私が昔書いたjQuery deferredに関するブログ記事をブックマークする人が、いまだにちょこちょこいるのが見受けられるのです。

もしかして、deferredの存在はなんとなく知っているけれど、どのように使ったらいいのかよくわからないという人が多いのかもしれないと思い、改めてまとまったものを書いてみることにしました。

本シリーズでは、jQuery deferredの基本的な部分と、どのような場面で使うのかという2点について解説します。

このシリーズのサンプルは、以下のレポジトリに置いてあります。

jQuery deferredサンプル集

pxgrid/codegrid-jQueryDeferred · GitHub

なお、この記事は、jQuery version 1.9.1の時点で書かれたものです。

jQuery deferredとはなにか

まずjQuery deferred(以降、deferred)とはなんなのかということから始めましょう。

deferredはjQueryに備わっている機能のひとつです。ですから、jQuery以外のなにか特別にプラグインのようなものを追加で読み込まなくても使えます。jQueryの機能の大部分は、DOMを操作するものとなっていますが、deferredはそうではありません。ざっくり言ってしまえば、deferredは、非同期処理を円滑に行うための、お助けツールのようなものです。deferredの説明はjQuery API Documentationの以下のページにあります。

deferredの説明

jQuery.Deferred() | jQuery API Documentation

しかしながら、このページを読んでみても、よくわからないかもしれません。私がこのページを初めて見たときは、そのように感じました。そこで具体的なサンプルを見つつ、理解を深めていくこととしましょう。

非同期とはどういうことか

deferredについて解説する前に「非同期」という言葉について説明しておきます。既知の方は適当に読み飛ばしてください。

JavaScriptを書いていく上で、非同期な処理を扱うのは避けて通れません。その非同期な処理というものとはどういうものなのか。筆者が例としてパッと思い浮かんだのは、setTimeoutと、$.ajaxです。まず、setTimeoutについて考えてみます。

alert('Hello');
setTimeout(function() {
  alert('2秒経った');
}, 2000);
alert('GoodBye');

このコードを実行すると、以下のような結果になります。

  1. 「Hello」とアラートされる
  2. 「GoodBye」とアラートされる
  3. 2秒後に「2秒経った」とアラートされる

コードをそのまま素直に受け止めると、2と3は逆になりそうな気がしますが、実際には、「2秒経った」とアラートされるのは最後になります。

次はjQueryの$.ajaxの例です。

alert('Hello');
$.ajax({
  url: 'something.txt',
  success: function() {
    alert('テキスト取ってきた');
  }
})
alert('GoodBye');

これも先ほどの例と同じで、次のような実行結果になります。

  1. 「Hello」とアラートされる
  2. 「GoodBye」とアラートされる
  3. ajaxが完了したら「テキスト取ってきた」とアラートされる

このような結果になるのは、setTimeout$.ajaxも、非同期な処理だからです。

setTimeout$.ajaxも、実行されたタイミングと、その処理が完了するタイミングが異なります。処理が完了するのは、いくらかの時間が経ってからになります。setTimeoutの例でアラートが出るのは、2秒経ってから、$.ajaxの例でアラートが出るのは、something.txtの取得が完了してからとなります。

ですが、アラート表示やテキスト取得の処理の完了を待つことなく、続けて書いたコードが実行されます。進行中のsetTimeout$.ajaxは、処理が完了した時点で、完了時に登録した関数が実行されます。非同期であるということは、このようなことをいいます。

これとは逆にalertは、同期的に処理されます。alertが出ると、ダイアログが表示され、そのダイアログを閉じるまで、続けて書かれたコードは実行されません。つまり処理が完了するまで待つわけです。

alert('Hello')は処理が終わるまで、次のsetTimeout()は実行されないが、setTimeout()は処理の終わりを待たずに、次の処理alert('GoodByd')が実行される。前者は同期的処理、後者は非同期的処理という。

このように、処理が終わるまで待つのが同期的な処理、処理が終わるまで待たないのが非同期な処理です。

コールバックを使う

このように完了を待たないのが非同期処理なわけですが、JavaScriptを書いていく上では、たくさんの非同期処理を扱わなければなりません。今挙げたajaxは基本、非同期で行いますし、jQueryでよくやる要素の幅や高さをアニメーションさせるのも、アニメーションが完了するのは、アニメーションを開始してからしばらく時間が経った時であり、その間にも続けて書いたコードが処理され続けるため、非同期な処理であるということができます。

このような非同期処理を行うときは、その処理が終わったときに、なにか処理を行わせたいことが多いです。

例えば$.ajaxであれば、当然、その内容がほしいから行うわけですし、setTimeoutも、時間が経った後になにかさせたいから使うわけでして。

このようなタイミングになにかさせる場合には、コールバックという仕組みを用います。例えば次のようなかたちです。なにかしらのWebサービスを想像してください。

// ユーザーIDをajaxで取得する関数
var getUserId = function(callback) {
  $.ajax({
    src: '/path/to/api',
    dataType: 'json',
    success: function(data) {
      callback(data.userId);
    }
  });
}
// ユーザーIDを取得し、アラートする
getUserId(function(userId) {
  alert('ユーザーIDは' + userId);
});

getUserIdは、ajaxでサーバーのAPIにアクセスし、自身のユーザーIDを取得するための関数であると想定します。この中で$.ajaxを使っています。ユーザーIDが得られるのは、ajax処理が完了した時であり、そしてこのajax処理自体が非同期な処理です。すぐに結果はわかりませんが、非同期な処理なので続けて書いたコードは実行されてしまいます。しかし、そうではなくて、そのユーザーIDを取得した時に続きの処理を行いたい。ではどうすればよいのか。

ここで使われるのが、コールバックという方法です。getUserId関数に、完了時に実行したい関数を引数として渡し、処理完了時にこれを実行します。この例ではgetUserId実行時に、引数として渡している無名関数が、その完了時に実行したい関数です。

getUserIdの中身を見てみると、$.ajax完了時に、この受け取った関数を実行しています。これがコールバックという仕組みであり、このように渡される関数は、コールバック関数と呼ばれます。

さらにいうと、$.ajaxに渡しているオブジェクトのsuccessに指定している関数も、コールバック関数です。ajax処理が完了したら、これを実行してくれという関数をsuccessに指定しています。時間を置いて結果がわかるような処理に対しては、すぐに結果を受け取ることができません。これを解決するのが、コールバックという仕組みです。JavaScriptでなにか書くときは、こんな具合にコールバックを多用することがあります。

ちなみにコールバックという言葉は、元来、電話のかけ直しのことをいうそうです。「オレ電話代払いたくないんで、ワンギリするからかけ直して」というのがコールバックらしいです。

コールバックの困るところ

このように非同期処理をうまく扱うために必要なのがコールバックですが、これを多用すると、困ることがあります。それは関数の入れ子が深くなりすぎてしまうということです。

例えば、ユーザーの親子兄弟関係を扱うようなサーバー側の仕組みがあったとします。これを用い、ユーザーIDを元に、その父親の母親の兄弟の情報(つまり大叔父や大叔母)を取得するケースを考えてみます。

サーバー側には、ユーザーIDから父親のIDを返すAPI母親のIDを返すAPI兄弟のIDを返すAPIの3つがあったとします。そんなとき、こんなコードを書くかもしれません。

getFatherId(userId, function(fatherId) {
  getMotherId(fatherId, function(motherId) {
    getBrothersIds(motherId, function(brothersIds) {
      console.log(brothersIds); // 兄弟のID
    });
  });
});

父親のIDを取得し、その母親のIDを取得し、その兄弟のIDを取得します。これらはすべて非同期の処理なので、今解説したコールバックの仕組みを使っているわけですが、これらは、ひとつひとつ順番を守って実行する必要があります。その結果、上記のようなコードになります。このように、コールバックという仕組みは便利なのですが、複雑な仕組みになると、だんだん入れ子が深くなり、可読性も下がりますし、書くのも辛くなってきます。

これを解決してくれるのがdeferredです。今の例をdeferredを使って書くと、次のように書けそうです。

getFatherId(userId)
  .then(getMotherId)
  .then(getBrothersIds)
  .then(function(brothersIds) {
    console.log(brothersIds); // 兄弟のID
  });

どちらがよいと、一概にいえるものではありませんが、deferredを使うと非同期な処理をシンプルに、統一したやり方で行うことができます。

前置きが長くなりましたが、そんなdeferredをサンプルを通して解説していきます。

doneとはなにか

1つめのサンプルでは、deferredの基本的な動作を確認してみます。

サンプル:01-done

codegrid-jQueryDeferred/01-done at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub

このサンプルを開き、ボタンをクリックすると、2秒後に「成功しました」とアラートが出ます。

var doSomething = function() {
  var defer = $.Deferred();
  setTimeout(function() {
    defer.resolve(); // 解決
  }, 2000);
  return defer.promise(); // プロミスを作って返す
};

$(function() {
  $('#button').on('click', function() {
    var promise = doSomething();
    promise.done(function() {
      alert('成功しました');
    });
  });
});

現場の実装では、たったそれだけのことをするために、わざわざこのサンプルのコードのような書き方はしませんが、deferredの動作を確認するため、このコード内で何が行われているのかを見ていきます。

deferredを作り、そのpromiseを返す

このコードのキモとなるのは、doSomethingという関数です。まず、この中では、以下のようなコードがあります。

  var defer = $.Deferred();

このように$.Deferredを実行すると、deferredオブジェクト(以降、deferred)が作られます。これを介していろいろと処理の連携をとるのが、deferredのやり方です。ちなみにここはnew $.Deferred()としても同じです。そして、ちょっとコードを飛ばしまして、この関数の最後returnしている部分を見てみます。

  return defer.promise(); // プロミスを作って返す

ここでは、今作ったdeferredのpromiseオブジェクト(以降、promise)を作り、返しています。

さて、いきなりdeferredとpromiseというものが出てきましたが、とりあえずこの段階では、詳しい説明は後回しにします。ここで作ったdeferredであるdeferは、非同期処理そのものを体現したオブジェクトであると考えてください。そして、このdeferredから作ったpromiseは、deferredのやり取りを外部と行うためのオブジェクトです。

解決された時の処理を登録する

次に、このdoSomethingを実行した箇所を見てみます。ボタンを押した後に実行される内容です。

    var promise = doSomething();

先ほどのdoSomethingは、promiseを返すのでした。ですので、変数promiseに入れておきます。このpromiseは次のように使います。

    promise.done(function() {
      alert('成功しました');
    });

ここでdoneが登場しています。promiseのdoneメソッドを呼ぶと、deferredが「解決」された時に、実行する関数を登録することができます。この場合「成功しました」というアラートが出ます。しかし、すぐには出ません。アラートが出るのは、あくまでもdeferredが「解決」された時です。

この部分の処理をまとめると、こんな具合になります。

  1. doSomethingを実行する
  2. いつ解決されるのかしらないけど、終わったら...
  3. 「成功しました」って出して

解決する

では、その「解決」されるとはどういうことか、doSomethingの中に戻ってみます。

var doSomething = function() {
  var defer = $.Deferred();
  setTimeout(function() {
    defer.resolve(); // 解決
  }, 2000);
  return defer.promise(); // プロミスを作って返す
};

2000ミリ秒後経ったあとに実行されているdefer.resolve()というのが、その解決するという動作を行っているところです。これが実行されると、promiseを通じてdoneに登録された関数が実行されます。これがdeferredの基本的な使い方です。

deferred図解

とりあえずdeferredを使ってみましたが、今起こったことを、図と併せて見てみましょう。

1. deferredの作成

まずdoSomethingが呼ばれ、deferredを新しく作りました。そして、そのpromiseを作り、これが返されました。promiseはdeferredと密接に関係しているオブジェクトです。

deferredには、状態(state)というものがあります。最初は、"pending"になっています。まだなにも起こっていないという状態です。

2. doneで関数を登録

そして、ここにdoneで「成功しました」と出るアラートを出す関数を登録しました。

3. 関数が登録される

すると、登録された関数をdeferredが持っている状態になります。まだ状態は"pending"のままです。

4. 解決する

2秒たったので、deferredがresolveされました。これで、このdeferredは解決された状態になります。

resolveが利用できるのは、deferredだけです。promiseにはresolveできません。promiseはdeferredの持っている機能のうち、状態を変化させる機能を使えなくしたオブジェクトであると考えておいてもらって問題ありません。

なぜわざわざpromiseを渡すのか

処理のメインとなる部分ではdeferredをいろいろと操作し、他の機能とやり取りする部分ではpromiseを渡すというのが、deferredを使った設計パターンです。もしdeferredをそのまま返してしまうと、その機能の外側で解決(resolve)や却下(後述するreject)をされてしまうかもしれないので、このようにします。

べつに、わざわざpromiseを作って返さずとも、promiseができることはdeferredにもすべてできます。実装上、promiseが持っているメソッドについては、deferredもまったく同じメソッドを持っているような状態になっています。

そういうわけなので、そのままdeferredを返してしまってもよいのですが、きっちり処理内容を切り分けるためにこのようにします。promiseは、deferredの状態が変わった時に行う処理を登録するためだけに使います。

なかば無理矢理ですが、promiseのことは「後で処理完了したら教えるから!」というdeferredの約束をオブジェクト化したものとでも思っておいてください。deferredとpromiseの関係は、親分と子分、社長と秘書とでも言いましょうか。抽象的な存在なので、ちょっと例えが難しいですね。

補足:deferredとpromiseでやっぱりもやもやする人へ

先ほどは例えを使ったりして、いろいろと説明しましたが、このdeferredとpromiseの関係は、言葉だけで理解するのは難しいものであると筆者は考えています。実際に自分でコードを書いてみると感じがつかめると思いますが、それ以上、どうなっているのか知りたければ、jQueryのコードを眺めてみるのがよいかと思います。ややこしいですが……。

5. 登録されていた関数が実行される

deferredがresolve(解決)されたので、状態は"pending"から"resolved"に変化します。これと同時にdoneで登録された関数が実行されます。

これが先ほどのサンプルの中で行っていた動作の一連の流れです。

failとはなにか

2つめのサンプルでは、deferredのメソッド、failを解説します。先ほどの例では、resolveで「解決」し、その時にdoneで登録した関数を実行させる例でした。一方、今度のサンプルはrejectで「却下」し、その時にfailで登録した関数を実行させています。

サンプル:02-fail

codegrid-jQueryDeferred/02-fail at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub

このサンプルを開き、ボタンをクリックすると、2秒後に「失敗しました」とアラートが出ます。

var doSomething = function() {
  var defer = $.Deferred();
  setTimeout(function() {
    defer.reject(); // 却下
  }, 2000);
  return defer.promise(); // プロミスを作って返す
};

$(function() {
  $('#button').on('click', function() {
    var promise = doSomething();
    promise.done(function() {
      alert('成功しました');
    });
    promise.fail(function() {
      alert('失敗しました');
    });
  });
});

なぜ、「失敗しました」と出るのでしょう。前のサンプルと異なる箇所を見てみます。

却下された時の処理を登録する

まずはボタンがクリックされた時の処理です。

    var promise = doSomething();
    promise.done(function() {
      alert('成功しました');
    });
    promise.fail(function() {
      alert('失敗しました');
    });

前のサンプルではdoneで関数を登録していましたが、今度はfailメソッドにも関数を渡しています。deferredは「解決」されたときとは別に、「却下」されたときにも、関数を登録できるのです。

その「却下」されたときに実行する関数を登録するのが、failメソッドです。今回は「失敗しました」と出ますから、どこかでdeferredが却下されたということになります。

却下する

今度はdoSomethingの中身を見てみます。

var doSomething = function() {
  var defer = $.Deferred();
  setTimeout(function() {
    defer.reject(); // 却下
  }, 2000);
  return defer.promise(); // プロミスを作って返す
};

先ほどresolve()になっていた箇所がreject()になっているのがわかります。このようにdeferredのrejectメソッドを呼ぶと、その処理を却下したことになります。deferredは、却下されると、failメソッドで登録した関数を実行します。

今回のサンプルでは、2秒経った時、かならずrejectされるようになっていますので、絶対失敗するという結果になります。ですが、実際のコードでは、なにか失敗する可能性のある処理に対して、成功時にはresolve、失敗時にはrejectをするという使い方になります。これについては追って見ていきます。

rejectされたときの状態

ちなみにrejectしたとき、deferredの状態は"rejected"になります。このdeferredの3つの状態、"pending""resolved""rejected"というのは、普段はたいして意識する必要がありませんが、deferred.state()といった具合に、deferredのstateメソッドを呼ぶと、この文字列が確認できます。

var defer = $.Deferred();
console.log(defer.state()); // "pending"
defer.resolve()
console.log(defer.state()); // "resolved"
var defer = $.Deferred();
console.log(defer.state()); // "pending"
defer.reject()
console.log(defer.state()); // "rejected"

promiseが呼べるメソッド

resolveと同様、rejectが使えるのは、deferredのみで、promiseにはできません。promiseが使えるのは、donefailと、次に紹介するthenだけであると覚えておいて、基本的には問題ありません。

thenとはなにか

3つめのサンプルでは、deferredのメソッドthenを解説します。

サンプル:03-then

codegrid-jQueryDeferred/03-then at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub

このサンプルの実行結果は、ひとつ前のサンプルと同じです。2秒後に「失敗しました」というアラートが出ます。

var doSomething = function() {
  var defer = $.Deferred();
  setTimeout(function() {
    //defer.resolve(); // 解決
    defer.reject(); // 却下
  }, 2000);
  return defer.promise();
};

$(function() {
  $('#button').on('click', function() {
    var promise = doSomething();
    promise.then(function() {
      alert('成功しました');
    }, function() {
      alert('失敗しました');
    });
  });
});

結果は同じですが、今回はdeferredのthenメソッドを使いました。

まとめて登録する

thenについては、そんなに解説することはありません。donefailを同時に登録できるだけと考えてもらってほとんど問題ありません。thenを使っている箇所を見てみましょう。

    var promise = doSomething();
    promise.then(function() {
      alert('成功しました');
    }, function() {
      alert('失敗しました');
    });

thenに2つの関数を渡しているのがわかります。1つめの関数がdone、2つめの関数がfailに渡す関数です。2つめの関数を指定しなければ、doneで登録したのと同じなので、基本、常にthenを使っておいて問題ないです。

補足:$.Deferredの初期化時の処理

$.Deferredは、初期化時に行わせたい処理を引数として渡すこともできます。例えばこのサンプルのdoSomethingは、次のように書いても同じです。

var doSomething = function() {
  return $.Deferred(function(defer) {
    setTimeout(function() {
      //defer.resolve(); // 解決
      defer.reject(); // 却下
    }, 2000);
  }).promise();
};

ajaxとはなにか

4つめのサンプルでは、ajaxとdeferredについて解説します。

サンプル:04-ajax

codegrid-jQueryDeferred/04-ajax at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub

今回のサンプルではボタンを押すと、外部ファイルのmessage.txtをajaxで取得し、その内容「Hello! NAME」をアラートしています。

message.txt

Hello! NAME

script.js

var getGreetingMessage = function() {
  var jqXHR = $.ajax({
    url: 'message.txt',
    dataType: 'text'
  });
  return jqXHR.promise(); // プロミスを作って返す
};

$(function() {
  $('#button').on('click', function() {
    getGreetingMessage()
      .then(function(message) {
        alert(message);
      }, function() {
        alert('メッセージの表示に失敗しました');
      });
  });
});

このコード内でなにが行われているのかを見ていきます。

ajaxとdeferred

このサンプルのgetGreetingMessage関数の中を見てみると、$.ajaxの結果からpromiseを作り、それを返しているのがわかります。これはdeferredなのでしょうか? 正確には異なりますが、おおむね、そのようなものだと認識しておいて問題ありません。

jQueryはバージョン1.5から、ajax周りの処理に、deferredを使うように変更されました。むしろ逆に、ajaxのような非同期処理をもっとうまく扱うために、deferredが実装されたと考えたほうが自然かもしれません。

$.ajaxの返り値は、jqXHRオブジェクトという、飛ばしたリクエストに関する情報が入っているオブジェクトです。このオブジェクトは、deferredの機能も持ち合わせています。このためpromiseを作り、今までと同様にthenで、リクエストの完了時に処理をすることができているのです。

ちなみに、いままでは、promiseを受け取った時、変数promiseへ一度保存していました。ですが、たいていの場合、そのあとthenなりdoneなりを繋げ、以降、使わないことが多いので、今回のサンプルでは、受け取ったpromiseに対し、そのままthenを繋げて書いています。

まとめ

今回は、jQuery deferredの基本的な部分やメソッドと、その前提となる概念について説明してきました。次回は、これを実際にこのような仕組みを、どのように実装に使っていくかという点について解説します。