prototype.jsを読み解く

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

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

1060:   onStateChange: function() {
1061:     var readyState = this.transport.readyState;
1062:     if (readyState > 1 && !((readyState == 4) && this._complete))
1063:       this.respondToReadyState(this.transport.readyState);
1064:   },
1065: 

1060行目からはonStateChange()メソッドです。

XHRが保持するreadyState変数を取得し,それがLoaded, Interactiveであるか,CompleteでまだonStateChange()が呼ばれていない場合にrespondToReadyState()メソッドを呼んでいます。

1066:   setRequestHeaders: function() {
1067:     var headers = {
1068:       'X-Requested-With': 'XMLHttpRequest',
1069:       'X-Prototype-Version': Prototype.Version,
1070:       'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
1071:     };
1072: 
1073:     if (this.method == 'post') {
1074:       headers['Content-type'] = this.options.contentType +
1075:         (this.options.encoding ? '; charset=' + this.options.encoding : '');
1076: 
1077:       /* Force "Connection: close" for older Mozilla browsers to work
1078:        * around a bug where XMLHttpRequest sends an incorrect
1079:        * Content-length header. See Mozilla Bugzilla #246651.
1080:        */
1081:       if (this.transport.overrideMimeType &&
1082:           (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
1083:             headers['Connection'] = 'close';
1084:     }
1085: 
1086:     // user-defined headers
1087:     if (typeof this.options.requestHeaders == 'object') {
1088:       var extras = this.options.requestHeaders;
1089: 
1090:       if (typeof extras.push == 'function')
1091:         for (var i = 0, length = extras.length; i < length; i += 2)
1092:           headers[extras[i]] = extras[i+1];
1093:       else
1094:         $H(extras).each(function(pair) { headers[pair.key] = pair.value });
1095:     }
1096: 
1097:     for (var name in headers)
1098:       this.transport.setRequestHeader(name, headers[name]);
1099:   },
1100: 

1066行目からはsetRequestHeaders()メソッドです。これは他の場所からも利用しうるから単独の関数にした,というのではなく,request()メソッドのコードの見通しをよくするために,まとまった部分を別メソッドにした,という様子です。

まず,デフォルトで送出されるヘッダとして,'X-Requested-With', 'X-Prototype-Version', 'Accept'を設定しています。

次に,メソッドが'post'の場合,options.contentTypeを使って'Content-Type'ヘッダを準備しています。985行目からで設定されているように,options.contentTypeのデフォルトは'application/x-www-form-urlencoded', options.encodingのデフォルトは'UTF-8'となっており,これで問題が無ければ呼び出す側は特にoptionsを上書き指定する必要はありません。

1077行目からは,古いMozilla系ブラウザにおいて,Content-Lengthヘッダが間違った値となってしまうバグに対処しています。XHRのoverrideMimeTypeはMozilla系のみのプロパティなので,それを確認し,UserAgent文字列のGecko/20040528のような文字列から古いことを確認した上で対処を入れています。

1086行目からはoptions.requestHeadersに指定されたヘッダをマージしています。

ここではHash形式のオブジェクトか配列が想定されているため,typeofが'object'で無ければ特に何もしません。

いったんextrasという変数に代入した後に,push()メソッドがあるかどうかで配列か,オブジェクトかを判断します。配列ならfor文で先頭から順に処理して,headersオブジェクトに追加します。オブジェクトなら$H()でHash化してeach()で各プロパティを列挙し,headersオブジェクトに追加しています。最後はObject.extend()の方がすっきりしそうではあります。

最後に1097行目から,headersの中身をすべて,XHRのsetRequestHeader()メソッドを使ってXHRインスタンスに設定します。

1101:   success: function() {
1102:     return !this.transport.status
1103:         || (this.transport.status >= 200 && this.transport.status < 300);
1104:   },
1105: 

1101行目からはsuccess()メソッドです。

XHRのstatusプロパティが偽なら偽を返し,status の値が2xx(成功)か3xx(リダイレクトなど)ならば真を返すようになっています。

statusプロパティが利用できるのは,readyStateが4(COMPLETED)の時のみなので,それ以前では呼ばないほうがいいでしょう。実装によっては3(INTERACTIVE)の時にもstatusが利用できるようですが,IEでは"because status and response headers are not fully available."といっているので,安全のためにも4(COMPLETED)になってからの方が良さそうです。

statusにまだ値が入っていなければ偽を返します。

1106:   respondToReadyState: function(readyState) {
1107:     var state = Ajax.Request.Events[readyState];
1108:     var transport = this.transport, json = this.evalJSON();
1109: 
1110:     if (state == 'Complete') {
1111:       try {
1112:         this._complete = true;
1113:         (this.options['on' + this.transport.status]
1114:          || this.options['on' + (this.success() ? 'Success' : 'Failure')]
1115:          || Prototype.emptyFunction)(transport, json);
1116:       } catch (e) {
1117:         this.dispatchException(e);
1118:       }
1119: 
1120:       var contentType = this.getHeader('Content-type');
1121:       if (contentType && contentType.strip().
1122:         match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
1123:           this.evalResponse();
1124:     }
1125: 
1126:     try {
1127:       (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
1128:       Ajax.Responders.dispatch('on' + state, this, transport, json);
1129:     } catch (e) {
1130:       this.dispatchException(e);
1131:     }
1132: 
1133:     if (state == 'Complete') {
1134:       // avoid memory leak in MSIE: clean up
1135:       this.transport.onreadystatechange = Prototype.emptyFunction;
1136:     }
1137:   },
1138: 

1106行目からはrespondToReadyState()メソッドです。

readyStateの各状態に対応して,Ajax.Respondersに登録されたイベント処理関数を実行するのが主な役割です。

まず,state変数に状態に応じたテキスト文字列をAjax.Request.Events配列から取得します。transportは何度も参照するので,短く書くためにローカル変数に代入しておきます。

jsonという変数に,this.evalJSON()の返り値を代入しています。これは,レスポンスヘッダにX-JSONヘッダが存在したら自動的にevalしてくれた結果を入れてくれるのですが,stateを判断する前にここでevalまで行ってしまうのは都合が悪い場合があるかもしれません。

1110行目からはstateが'Complete'の時の処理です。XHRでは,ブラウザの互換性の問題もあり,何か処理をするとしたらほぼこのタイミングで行うことになるでしょう。

this._completeフラグをtrueに設定し,再度respondToReadyState(4)が呼ばれないようにし,イベント処理関数を呼び出します。

ここでは'on200'などのステータスコードに関連付けられたイベント処理関数がoptionsに登録されていればそれを優先し,それが無ければ'onSuccess'か'onFailure'があるかどうかを調べて,それも無ければ最終的にPrototype.emptyFunctionが使われます('onComplete'などはもう少し後で呼ばれます⁠⁠。

1120行目からは,レスポンスヘッダのContent-Typeを取得し,それが

  • application/ecmascript
  • application/javascript
  • application/x-ecmascript
  • application/x-javascript
  • text/ecmascript
  • text/javascript
  • text/x-ecmascript
  • text/x-javascript

ならevalResponse()メソッドを使ってレスポンスボディをJSONとみなしeval()します。

1126行目からは,'onComplete'だけでなく全ての状態に対する処理です。optionsに'onComplete'や'onLoading'などのイベント処理関数が登録されていればそれを実行し,Ajax.Respondersに登録されているものも続けて実行します。

最後に,著明なIEのメモリリークの問題に対応するために,onreadystatechangeに登録された関数をクリアしておきます。

著者プロフィール

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

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