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 である |