prototype.jsを読み解く

第4回 Prototypeライブラリ(932~1289行目)

この記事を読むのに必要な時間:およそ 15.5 分

Ajax.Requestクラス

1000: Ajax.Request = Class.create();
1001: Ajax.Request.Events =
1002:   ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
1003: 

1000行目からはAjax.Requestクラスです。

いつもどおりClass.create()で初期設定をし,後で使うために文字列定数の配列Eventsを定義しておきます。

1004: Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
1005:   _complete: false,
1006: 
1007:   initialize: function(url, options) {
1008:     this.transport = Ajax.getTransport();
1009:     this.setOptions(options);
1010:     this.request(url);
1011:   },
1012: 

1004行目からAjax.Requestのprototypeを構築します。

まずはObject.extend()を使って,Ajax.BaseのsetOptions()メソッドを継承します。

ここではまず,new Ajax.Base()してインスタンスを作成しています。このnewによりprototype.setOptionsを持った新しいオブジェクトがメモリ上に作られ,それに対して_completeなどの属性を追加しています。

これにより,Ajax.Base.prototypeのオリジナルはそのままで,Ajax.Request.prototypeとして独自にプロパティを追加することができます。

_complete変数は,onStateChangeで状態が4 (Complete)になって以降,重複してイベント処理関数が呼ばれないようにするためのフラグです。

1007行目からはコンストラクタです。先に定義したAjax.getTransport()を使って,新しいXHRインスタンスを取得します。setOptions()を使って渡されたオプションでデフォルトを上書きし,最後にXHR通信を行うrequest()メソッドを呼び出します。

1013:   request: function(url) {
1014:     this.url = url;
1015:     this.method = this.options.method;
1016:     var params = Object.clone(this.options.parameters);
1017: 
1018:     if (!['get', 'post'].include(this.method)) {
1019:       // simulate other verbs over post
1020:       params['_method'] = this.method;
1021:       this.method = 'post';
1022:     }
1023: 
1024:     this.parameters = params;
1025: 
1026:     if (params = Hash.toQueryString(params)) {
1027:       // when GET, append parameters to URL
1028:       if (this.method == 'get')
1029:         this.url += (this.url.include('?') ? '&' : '?') + params;
1030:       else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
1031:         params += '&_=';
1032:     }
1033: 
1034:     try {
1035:       if (this.options.onCreate) this.options.onCreate(this.transport);
1036:       Ajax.Responders.dispatch('onCreate', this, this.transport);
1037: 
1038:       this.transport.open(this.method.toUpperCase(), this.url,
1039:         this.options.asynchronous);
1040: 
1041:       if (this.options.asynchronous)
1042:         setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
1043: 
1044:       this.transport.onreadystatechange = this.onStateChange.bind(this);
1045:       this.setRequestHeaders();
1046: 
1047:       this.body = this.method == 'post' ? (this.options.postBody || params) : null;
1048:       this.transport.send(this.body);
1049: 
1050:       /* Force Firefox to handle ready state 4 for synchronous requests */
1051:       if (!this.options.asynchronous && this.transport.overrideMimeType)
1052:         this.onStateChange();
1053: 
1054:     }
1055:     catch (e) {
1056:       this.dispatchException(e);
1057:     }
1058:   },
1059: 

実際のXHR通信を行うrequest()メソッドです。直接渡される引数はurlだけですが,optionsを使って様々な制御を行っています。

まず1014~1015行目でurlとmethodをインスタンス内に格納します。ここで関数ローカルの変数として定義していないのは,この関数から呼び出される別のメソッドでも使われうるため,この形で保存しておく方が便利なためです。

1016行目では,送出するパラメータを構築するのにparams変数にparametersオブジェクトをコピーしています。元のoptions.parametersはその後使っていないようなので,ここはわざわざObject.clone()を使う必要は無さそうには見えます。何らかの名残りでしょうか。

1018行目からは,今回のmethod指定が'get' でも 'post'でもない場合,'post'相当の挙動を行うようにしています。その際送出するパラメータに_methodという名前でメソッド文字列を渡すようにしています。

1024行目でこの時点でのparamsをthis.parametersに保存していますが,これも他では参照されていないようです。

1024行目からで,最終的な文字列としてparamsを構築します。この段階でparamsはHash形式のオブジェクトなので,それをquery string形式に変換します。もし返ってくる文字列が空でなければ,if文の中の処理を行います。

メソッドが'get'の場合,すでにurlに'?'が含まれていれば'&'で,含まれていなければ'?'でurlとparams文字列を結合します。

メソッドが'post'の際に,Konqueror,Safari,KHTMLのいずれかの時には,paramsにダミーとして'&_='を追加しています。

この部分はRailsのSubversionリポジトリに入った当初からあったようで,理由は追いきれませんでした。

1034行目からいよいよXHRの実行です。全体をtry {}で括って,後でまとめて例外処理を行っています。

まず,options.onCreateが指定されていれば,それを呼び出します。

次に,登録されている'onCreate'イベント処理を実行するために,Ajax.Responders.dispatch()を呼び出します。

1038行目でXHRのopen()メソッドを呼んでいます。

1041行目において,非同期通信の場合には10ms後に'Loading'状態に移行されるようにタイマーをセットします。

XHRのonreadystatechangeイベントハンドラには,Ajax.Request.onStateChangeの関数が,今使っているAjax.Requestインスタンスがthisになるようにbind()して設定しています。

1045行目で,後述するthis.setRequestHeaders()を呼び出して,細かいリクエストヘッダの調整をします。

1047行目では,もしメソッドが'post'ならoptions.postBodyに指定されたものをthis.bodyに用意します。もし'post'なのにoptions.postBodyが指定されていない場合には,先ほど構築したparams文字列を代入しておきます。'get'の場合はthis.bodyにはnullを入れておきます。XHRのsend()メソッドにundefinedを渡してしまうと,FirefoxなどMozilla系でエラーになってしまうため,のようです。

1048行目で,やっとXHRのsend()関数を呼び出して送信終了です。

1050行目からは,Firefoxが同期通信の際に'Complete'状態に設定してくれない,という問題に対応するために,明示的にonStateChange()を呼んでいます。そもそもMozillaの仕様ではonStateChangeが呼ばれるのは非同期の時のみ,となっているので,何か違うことに対処するためか,単にブラウザ毎の挙動を統一するためか,かもしれません。 transport.overrideMimeTypeにメソッドが存在するかどうかがMozilla系のチェックとなっています。

try{}内で例外が発生した場合,dispatchException()メソッドを呼び出します。これによりoptions.onExceptionが設定されていればそれが呼び出され,Ajax.Respondersに'onException'イベント処理関数が登録されていればそれが実行されるようになっています。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。