prototype.js の Ajax

prototype.js 1.6 がリリースされましたね。今日は、Ajax クラスのお話です。

prototype.js を利用しない場合

prototype.js を利用しないで、XMLHttpRequest オブジェクトを非同期に利用する場合、典型的には以下のようなコードになります。この JavaScript のコードが置いてあるサーバから、ファイル foo.txt を取ってきて表示します。

var request = new XMLHttpRequest();
request.onreadystatechange = function() {
    if (request.readyState == 4) {
	alert(request.responseText);
    }
}
request.open('GET', 'foo.txt', true);
request.send(null);

readyState の意味は以下の通りです。

0 open() はまだ呼び出されていない
1 open() は呼び出されたが、send()は呼び出されていない
2 send() は呼び出されたが、まだサーバは応答してない
3 サーバからデータを受信中である
4 データの取得が完了した

通常のプログラムでは、取得が完了したデータを扱いますから、readyState が 4 のときのコードを書くことになります。

こういう風に XMLHttpRequest オブジェクトを直接操作するときの問題点は、以下の通りです。

  • IE 7 より前の IE では、XMLHttpRequest はなく、ActiveXObject を利用する必要がある。
  • readyState に覚えにくい数字を使わなければならない

prototype.js の Ajax オブジェクト

上記の問題は、prototype.js の Ajax.Request クラスを使うと解決できます。上記のコードは、以下のように書けます。

new Ajax.Request('foo.txt', {
    method: 'get',
    onSuccess: function(transport) {
	alert(transport.responseText);
    }
});

クロス・ブラウザ

XMLHttpRequest の問題は、Ajax.getTransport で解決されています。どの関数が実装されていてもいいように、順番に試していっている訳です。

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

数字の文字列化

数字は以下のように文字列に置き換えられています。

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

これらの文字列は、利用する際に先頭に 'on' が付けられます。上記の表とマージすると、こうなります。

0 onUninitialized open() はまだ呼び出されていない
1 onLoading open() は呼び出されたが、send()は呼び出されていない
2 onLoaded send() は呼び出されたが、まだサーバは応答してない
3 onInteractive サーバからデータを受信中である
4 onComplete データの取得が完了した

するどい人ならお気づきでしょうが、readyState 4 は onSuccess ではなく、onComplete と定義してあります。一体どういうことでしょうか? 今日の本題は、この謎を探ることです。

Ajax.Request.respondToReadyState

Success などのキーワードで、Ajax.Request クラスの中を検索してみると、respondToReadyState が見つかります。これを読んでいきましょう。

Ajax.Request = Class.create(Ajax.Base, {
...
  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },
...
};

state と response

まず変数です。

    var state = Ajax.Request.Events[readyState];
    var response = new Ajax.Response(this);

state には readyState を文字列に直したものが入ります。response には、サーバの応答を Ajax.Response クラスで抽象化したオブジェクトが入ります。詳細は知る必要はありません。

Complete state (1)

state が 'Complete' のとき、まず以下のコードが呼ばれます。

        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);

これは、関数呼び出しなのですが、分りにくいので補助変数を使って書き換えてみましょう。

        var successOrFailure = this.success() ? 'Success' : 'Failure';
        var func = this.options['on' + response.status] ||
                   this.options['on' + successOrFailure] ||
                   Prototype.emptyFunction;
        func(response, response.headerJSON);

response.status は '200' のような HTTP の応答コードです。なので、'on200' のような関数が定義されていたら、その関数が採用されます。

関数 success の定義は以下の通りです。

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

よって、HTTP の応答コードが、200 以上で、かつ 300 より小さければ、'Success' になります。そうでなければ、'Failure' になります。それぞれの場合に、それぞれに対応する 'onSuccess'/'onFailure' 関数が定義されていれば、その関数が採用されます。

'on200' などや 'onSuccess'/'onFailure' が定義されない場合、何もしない関数が採用されます。

採用された関数が、response と response.headerJSON を引数として実行されるのです。

このようにして、ここでは onComplete ではなく onSuccess が実行されるわけです。

Complete state (2)

state が Complete のとき、さらに以下のコードが実行されます。

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();

すなわち、evalJS オプションを 'force' にしておくか、evalJS が true (デフォルト)かつ、Content-Type: が text/javascript などであれば、サーバから受け取ったデータを JavaScript のコードだと思って実行します。

最後の部分

次に、state の値がなんであろうと、以下のコードを通過します。

      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);

ここで、'onLoaded' や 'onComplete' が定義されていれば実行されることになります。'onSuccess' と 'onComplete' の両方が定義されており、サーバとの通信が成功した場合、両方の関数が実行されることに注意しましょう。

その他

state は別に、XMLHttpRequest オブジェクトなどが作られたときに呼ばれる 'onCreate' という関数も定義できます。

まとめ

- onCreate Ajaxオブジェクトが作られた
0 onUninitialized open() はまだ呼び出されていない
1 onLoading open() は呼び出されたが、send()は呼び出されていない
2 onLoaded send() は呼び出されたが、まだサーバは応答してない
3 onInteractive サーバからデータを受信中である
4 onComplete データの取得が完了した
- onSuccess データの取得が成功した(200番台)
- onFailure データの取得が失敗した
- onXYZ データの取得の結果が XYZ である