prototype.jsを読み解く

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

リポジトリの差分を追う

第四回目です。

前回、Subversionのログを見るためのTracのリポジトリブラウザを紹介しました。単にログを見る場合、時系列にコミットログを追っていくことはできますが、実際に知りたいのは、このコード片の変更がどのリビジョンで行われたか、でしょう。

これはSubversionのannotateサブコマンドで確認することができます。

% svn annotate http://svn.rubyonrails.org/rails/spinoffs/prototype/trunk/src/prototype.js
  3362        sam <%= include 'HEADER' %>
  3362        sam
  3362        sam var Prototype = {
...

これでその行が最後に変更されたリビジョンがわかるので、そのログや差分を確認します。そのリビジョンが目当てとする変更ではなかった場合、"svn annotate -r 一つ前のリビジョン" を使ってさらにさかのぼっていきます。

最終的に変更点のリビジョンを突き止めたら、そのログやTrac Ticket番号を頼りに内容を理解する、という形になります。

では、今回はAjaxオブジェクトからです。

Ajaxオブジェクト

0932: var Ajax = {
0933:   getTransport: function() {
0934:     return Try.these(
0935:       function() {return new XMLHttpRequest()},
0936:       function() {return new ActiveXObject('Msxml2.XMLHTTP')},
0937:       function() {return new ActiveXObject('Microsoft.XMLHTTP')}
0938:     ) || false;
0939:   },
0940: 
0941:   activeRequestCount: 0
0942: }
0943: 

Ajaxは複数のクラスを包含するための名前空間となっており、まずAjax直下にはヘルパ関数getTransport()と、静的変数activeRequestCountが定義されています。

getTransport()関数は、前述したTry.these()関数を使い、複数のアクションを連続して試みています。935行目からにあるように、最初にXMLHttpRequest()をnewしてみて、成功したらそのインスタンスを返します。残りの古いIE用のActiveXObjectの生成も同様です。Try.these()にあるうちの、いずれも失敗した場合には最終的にfalseが返ります。

941行目のactiveRequestCountは、後述するAjax.Respondersで使われます。

0944: Ajax.Responders = {
0945:   responders: [],
0946: 
0947:   _each: function(iterator) {
0948:     this.responders._each(iterator);
0949:   },
0950: 

Ajax.RespondersはXHR接続に対するイベントを管理するオブジェクトです。

register()、unregister()で利用するために、静的な配列respondersを空で初期化しています。

Ajax.Respondersは後でEnumerableをmixinするため、Enumerable.each()から呼び出される_each()メソッドを実装しています。

948行目では、945行目で定義したresponders配列のArray._each()を呼び出しています。

0951:   register: function(responder) {
0952:     if (!this.include(responder))
0953:       this.responders.push(responder);
0954:   },
0955: 
0956:   unregister: function(responder) {
0957:     this.responders = this.responders.without(responder);
0958:   },
0959: 

951行目からのregister()関数では、引数として渡されたresponderがAjax.Responders.responders配列に含まれているかを確認し、無ければpush()して追加します。

ここで、Ajax.Respondersはnewして使うようなクラスの実装とはなっていません。使う場合はAjax.Responders.register(...)という形で呼び出します。

このとき、register()関数のthisはグローバルなAjax.Respondersオブジェクトを示します。そのため、953行目のthis.respondersは、945行目で定義しているresponders配列を示すことになります。

956行目からはunregister()関数です。Array.without()メソッドを使って、指定されたものが含まれないresponders配列を作り直しています。

0960:   dispatch: function(callback, request, transport, json) {
0961:     this.each(function(responder) {
0962:       if (typeof responder[callback] == 'function') {
0963:         try {
0964:           responder[callback].apply(responder, [request, transport, json]);
0965:         } catch (e) {}
0966:       }
0967:     });
0968:   }
0969: };
0970: 

960行目からのdispatch()関数は、Prototypeライブラリ内部で利用される関数です。登録されたイベント処理関数を要求されたイベントタイプに基づいて呼び出す仕事をします。

最初の引数callbackには、'onCreate', 'onUninitialized', 'onLoading', 'onLoaded', 'onInteractive', 'onSuccess'/'onFailure', 'onComplete'などの文字列が渡されます。request、transport、jsonは、イベント処理関数に渡されるパラメータです。

961行目のeach()で、responders配列に含まれるすべてのオブジェクトに対してループ処理をします。その中で、登録されたオブジェクトが、'onCreate'などのイベント名に対応したプロパティをもっており、かつそれが関数オブジェクトだった場合に、apply()を使って関数を呼び出します。apply()の第一引数がresponderなので、これがイベント処理関数の中ではこれがthisとなります。

0971: Object.extend(Ajax.Responders, Enumerable);
0972: 

Ajax.RespondersにEnumerableをmixinして、Enumerableとして振舞うようにしています。

0973: Ajax.Responders.register({
0974:   onCreate: function() {
0975:     Ajax.activeRequestCount++;
0976:   },
0977:   onComplete: function() {
0978:     Ajax.activeRequestCount--;
0979:   }
0980: });
0981: 

973行目で、Ajax.Responders.register()を呼び出し、最初のresponderを登録しています。

これにより、Ajax.Responders.dispatch('onCreate', ...)が呼び出されると、Ajax.activeRequestCountがインクリメントされ、'onComlete'の際にはデクリメントされることになります。dispatch()はPrototypeライブラリ内のXHR呼び出しを管理する部分で自動的に呼び出してくれるので、このregister()だけでこのような処理が可能となります。

0982: Ajax.Base = function() {};
0983: Ajax.Base.prototype = {
0984:   setOptions: function(options) {
0985:     this.options = {
0986:       method:       'post',
0987:       asynchronous: true,
0988:       contentType:  'application/x-www-form-urlencoded',
0989:       encoding:     'UTF-8',
0990:       parameters:   ''
0991:     }
0992:     Object.extend(this.options, options || {});
0993: 
0994:     this.options.method = this.options.method.toLowerCase();
0995:     if (typeof this.options.parameters == 'string')
0996:       this.options.parameters = this.options.parameters.toQueryParams();
0997:   }
0998: }
0999: 

他のAjax.*クラスで利用するための、Ajax.Baseというオブジェクトを定義しています。

982行目でコンストラクタ用に空の関数を定義して、後はprototypeプロパティ以下にsetOptions()というメソッドを定義しているだけです。

ここでは、インスタンスのthis.optionsというプロパティにデフォルトの値のセットを定義し、引数経由で渡されたオブジェクトで上書きしています。Object.extend()の第一引数にデフォルトとなる値を集めたオブジェクトを渡し、第二引数で関数利用者が必要なものだけを指定して上書きする、という形は常用される定型句です。

994行目ではoptions.methodを小文字に正規化しています。

options.parametersは、文字列でも、Hash互換のオブジェクトでも受け付けるようになっているので、もし文字列だった場合にはtoQueryParams()で一旦Hashオブジェクトの形式に変換しておきます。

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'イベント処理関数が登録されていればそれが実行されるようになっています。

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に登録された関数をクリアしておきます。

1139:   getHeader: function(name) {
1140:     try {
1141:       return this.transport.getResponseHeader(name);
1142:     } catch (e) { return null }
1143:   },
1144: 

1139行目からはgetHeader()メソッドです。

もしLoading, Complete状態以前にXHR.getResponseHeader()を呼び出すと例外が発生するので、その場合はtry catchしてnullを返しておきます。

例外が発生しなければgetResponseHeader()の返り値をそのまま返します。

1145:   evalJSON: function() {
1146:     try {
1147:       var json = this.getHeader('X-JSON');
1148:       return json ? json.evalJSON() : null;
1149:     } catch (e) { return null }
1150:   },
1151: 

1145行目からはevalJSON()メソッドです。

すぐ上のgetHeader()メソッドを使ってレスポンス内にX-JSONヘッダがあるかどうかを調べます。

もしあるようならString.evalJSON()を使ってその結果を返します。ないようならnullを返しています。

1152:   evalResponse: function() {
1153:     try {
1154:       return eval((this.transport.responseText || '').unfilterJSON());
1155:     } catch (e) {
1156:       this.dispatchException(e);
1157:     }
1158:   },
1159: 

1152行目からはevalResponse()メソッドです。レスポンスボディ(this.transport.responseText)に対して、念のためString.unfilterJSON()を呼び出し、その結果をeval()します。

ほぼ似たようなことをしているので、String.evalJSON()を使ってもいいような気はします。

1160:   dispatchException: function(exception) {
1161:     (this.options.onException || Prototype.emptyFunction)(this, exception);
1162:     Ajax.Responders.dispatch('onException', this, exception);
1163:   }
1164: });
1165: 

Ajax.Requestの最後はdispatchException() メソッドです。optionsにonExceptionイベント処理関数が登録されていればそれを呼び出し、Ajax.Respondersに登録されている'onException'処理関数が実行されるようにします。

Ajax.Updaterクラス

1166: Ajax.Updater = Class.create();
1167: 

1166行目からはAjax.Updaterクラスです。まずはいつもどおりにClass.create()で雛型を作ります。

1168: Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
1169:   initialize: function(container, url, options) {
1170:     this.container = {
1171:       success: (container.success || container),
1172:       failure: (container.failure || (container.success ? null : container))
1173:     }
1174: 
1175:     this.transport = Ajax.getTransport();
1176:     this.setOptions(options);
1177: 
1178:     var onComplete = this.options.onComplete || Prototype.emptyFunction;
1179:     this.options.onComplete = (function(transport, param) {
1180:       this.updateContent();
1181:       onComplete(transport, param);
1182:     }).bind(this);
1183: 
1184:     this.request(url);
1185:   },
1186: 

1168行目ではまずObject.extend()を使ってAjax.Request.prototypeを継承し、そのままAjax.Updater.prototypeの拡張を開始します。

1169行目からのコンストラクタでは、最初の引数containerとして、単純な要素、もしくはそのIDを渡すこともできますし、複雑な{ success:'要素1', failure:'要素2' }というオブジェクトを渡すこともできます。1170行目では、container引数を後者の形に正規化しています。

1175行目、1176行目、1184行目はAjax.Requestのコンストラクタと同様です。

1178行目からは、ユーザーが指定したonCompleteイベント処理関数の前に、Ajax.Updater独自のthis.updateContent()を呼び出すようにしています。

まず、通常のイベント処理関数の形(function(transport, param){})で関数オブジェクトをつくり、その中身はupdateContent()の呼び出しとそれに続くオリジナルのonComplete()の呼び出しです。作成した関数オブジェクトのthisが自分自身のインスタンスになるようにbind()して、それを元々のthis.options.onCompleteに再代入しています。

options.onCompleteの呼び出し自体は、Ajax.Requestで定義されているrequest()経由で行われ、Ajax.UpdaterではonCompleteに関してはそれ以上のことは行いません。

1187:   updateContent: function() {
1188:     var receiver = this.container[this.success() ? 'success' : 'failure'];
1189:     var response = this.transport.responseText;
1190: 
1191:     if (!this.options.evalScripts) response = response.stripScripts();
1192: 
1193:     if (receiver = $(receiver)) {
1194:       if (this.options.insertion)
1195:         new this.options.insertion(receiver, response);
1196:       else
1197:         receiver.update(response);
1198:     }
1199: 
1200:     if (this.success()) {
1201:       if (this.onComplete)
1202:         setTimeout(this.onComplete.bind(this), 10);
1203:     }
1204:   }
1205: });
1206: 

1187行目からはAjax.UpdaterのキモであるupdateContent()メソッドです。これはAjax.RequestのonCompleteのタイミングで呼び出されます。

まずは、1188行目で、this.success()で成功か失敗かの判別をした上で、更新が行われる要素をreceiverに代入します。また、this.transport.responseTextは何度も利用するので短く記述するためにresponse変数に入れておきます。

1191行目では、options.evalScriptsフラグがセットされていなければ、<script>タグを除去してしまいます。そのため、XHR経由で戻ってくるHTMLコンテンツに <script>タグを含めたい場合にはoptions.evalScriptsフラグを設定しておく必要があります。

1193行目で、receiverに入っている要素を$()関数を使ってElementオブジェクトに変換します。もし指定された要素が存在しなければこのif文はスキップされます。

options.insertionが指定されている場合、Insertion.BottomなどのInsertionオブジェクトが入っているはずなので、newでインスタンスを生成して駆動します。

options.insertionが指定されていなければ、要素receiverのupdate()メソッド(Element.update)を呼び出して要素を更新します。

1200行目からは、success()メソッドが真の時に、this.onCompleteがセットされていればそれを呼び出します。しかし現状では、Prototypeライブラリ内ではこの機能は使われていないようです。

Ajax.PeriodicalUpdaterクラス

1207: Ajax.PeriodicalUpdater = Class.create();
1208: Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
1209:   initialize: function(container, url, options) {
1210:     this.setOptions(options);
1211:     this.onComplete = this.options.onComplete;
1212: 
1213:     this.frequency = (this.options.frequency || 2);
1214:     this.decay = (this.options.decay || 1);
1215: 
1216:     this.updater = {};
1217:     this.container = container;
1218:     this.url = url;
1219: 
1220:     this.start();
1221:   },
1222: 

1207行目からはAjax.PeriodicalUpdaterです。

例のごとくClass.create()で雛型を作成し、Object.extend()でAjax.Baseから継承しています。

コンストラクタではsetOptions()を使ってthis.optionsを設定した後、this直下のインスタンス変数に、いくつかの変数をoptionsから設定しています(その際にデフォルト値も指定しています⁠⁠。this.onCompleteは、stop()メソッドを使ってAjax.PeriodicalUpdater自体が終了するときに呼び出されます。options.onCompleteは定期的に呼ばれるAjax.Updaterで使うために別途上書きされます。

最後に周期的なタイマーを開始するためにthis.start()を呼び出します。

1223:   start: function() {
1224:     this.options.onComplete = this.updateComplete.bind(this);
1225:     this.onTimerEvent();
1226:   },
1227: 

まず、options.onCompleteにthis.updateCompleteを入れておきます。この後Ajax.Updaterで単一の更新処理を実行しますが、その際にthis.optionsを渡します。Ajax.Updaterのoptions.onCompleteの処理時に、ここで代入したAjax.PeriodicalUpdater.updateComplete()メソッドが呼ばれることになります。

その設定が終わると、初回の更新処理をonTimerEvent()で実行します。

1228:   stop: function() {
1229:     this.updater.options.onComplete = undefined;
1230:     clearTimeout(this.timer);
1231:     (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
1232:   },
1233: 

1228行目からはstop()メソッドです。定期的に行われている更新処理を中断します。

もしAjax.Updaterが実行中の場合、Ajax.Updaterインスタンスはthis.updaterに入っていますので、ここのoptions.onCompleteをundefinedにしておいて、更新完了時にupdateComplete()が呼ばれないようにします。次に、clearTimeout()で保存してある待機中のタイマーをキャンセルします。

最後に保存しておいたthis.onComplete関数を実行します。この時にargumentsを渡していますので、onCompleteにはstop()に渡された引数がそのまま渡されることになります。公式APIドキュメントにはstop()は引数がない、となっていますので、ここは混乱をさけるためにもargumentsは渡さない方がいい気がします。

1234:   updateComplete: function(request) {
1235:     if (this.options.decay) {
1236:       this.decay = (request.responseText == this.lastText ?
1237:         this.decay * this.options.decay : 1);
1238: 
1239:       this.lastText = request.responseText;
1240:     }
1241:     this.timer = setTimeout(this.onTimerEvent.bind(this),
1242:       this.decay * this.frequency * 1000);
1243:   },
1244: 

1234行目からはupdateComplete()メソッドです。これはstart()経由でonTimerEvent()でnew Ajax.Updater()に渡されています。その結果、一回のAjax.Updater処理が終わったタイミングで呼び出されます。

1235行目からはoptions.decayの処理です。⁠前回とレスポンスが変わっていなければ徐々に待ち時間を増やす」という挙動のために、this.lastTextに前回のresponseTextを保存しておき、今回のと比較して同じなら掛け算でthis.decayを増やし、違っていたら1に初期化しています。

そして1241行目で次のタイマーをセットしています。タイマー終了後に呼び出されるのはonTimerEventで、thisがこのインスタンスを示すようにbind(this)しておきます。

1245:   onTimerEvent: function() {
1246:     this.updater = new Ajax.Updater(this.container, this.url, this.options);
1247:   }
1248: });

最後はonTimerEvent()メソッドです。

初回、もしくはタイマー処理で呼ばれる関数で、単純にAjax.Updater()を作って、後で参照するためにthis.updaterにインスタンスを代入しているだけです。

1249: function $(element) {
1250:   if (arguments.length > 1) {
1251:     for (var i = 0, elements = [], length = arguments.length; i < length; i++)
1252:       elements.push($(arguments[i]));
1253:     return elements;
1254:   }
1255:   if (typeof element == 'string')
1256:     element = document.getElementById(element);
1257:   return Element.extend(element);
1258: }
1259: 

1249行目からは、有名な$()関数です。

まず、引数に複数の値が渡された場合の処理です。ループ用変数iと返り値用の配列elements, 何度もarguments.lengthを評価するのを避けるためにしまっておくためのlengthを初期化して、通常のforループに入ります。ループの中では単に$()単一の引数を付けてを呼び出し、その返り値をelementsにpush()し、最後にそれをまとめてreturnで返しています。

1255行目では、引数elementが文字列かどうかを確認し、そうであればID文字列とみなしてdocument.getElementById()を呼び出します。文字列でなければelementは特に触りません。

最後に、Element.extend()を使ってPrototypeライブラリで独自に追加しているメソッドを追加しています。

1260: if (Prototype.BrowserFeatures.XPath) {
1261:   document._getElementsByXPath = function(expression, parentElement) {
1262:     var results = [];
1263:     var query = document.evaluate(expression, $(parentElement) || document,
1264:       null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1265:     for (var i = 0, length = query.snapshotLength; i < length; i++)
1266:       results.push(query.snapshotItem(i));
1267:     return results;
1268:   };
1269: 
1270:   document.getElementsByClassName = function(className, parentElement) {
1271:     var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
1272:     return document._getElementsByXPath(q, parentElement);
1273:   }
1274: 
1275: } else document.getElementsByClassName = function(className, parentElement) {
1276:   var children = ($(parentElement) || document.body).getElementsByTagName('*');
1277:   var elements = [], child, pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
1278:   for (var i = 0, length = children.length; i < length; i++) {
1279:     child = children[i];
1280:     var elementClassName = child.className;
1281:     if (elementClassName.length == 0) continue;
1282:     if (elementClassName == className || elementClassName.match(pattern))
1283:       elements.push(Element.extend(child));
1284:   }
1285:   return elements;
1286: };
1287: 
1288: /*--------------------------------------------------------------------------*/
1289: 

1260行目からはdocument.getElementsByClassName()関数です。

ブラウザ側でXPathが使えるかどうかによって実装が変わっています。

1261行目からはXPathが使える場合です。まずは、document._getElementsByXPath()というヘルパ関数を用意します。返り値用にresultsを空で初期化した後で、document.evaluate()でXPathを使って要素をピックアップします。この呼び出しの細かい意味は、DOM 3 XPathを参照してください。

evaluate()はXPathResult型を返します。snapshotLengthプロパティと、snapshotItem()メソッドで、マッチした要素全てを取得することができるので、それを集めて返します。

1270行目からdocument.getElementsByClassName()の実装です。先ほどのdocument._getElementsByXPath()を使って、指定されたクラス名を持つ要素を列挙しています。

ここで、変数qに代入されているXPath式を簡単に解説すると、

意味
.//* コンテキストノード(XMLツリー内での現在位置)の子孫のうちの任意の要素
[contains( foo, bar )] ……のうちfooがbarを含んでいるもの
concat(' ', @class, ' ') 渡された文字列を連結して返す。@classはその要素のclass 属性
' ' + className + ' ' 変数classNameの前後に空白を加えたもの

という形になっています。これにより「指定された要素の子孫のうち、渡されたクラス名をclass属性に含む任意の要素」を返すための式となっています。

1275行目からは、XPathが使えない場合のgetElementsByClassName()の実装です。

まず、指定されたparentElement(指定されていなければdocument.body)を頂点にして、getElementsByTagName("*")を使ってぶら下がる要素を全て列挙してchildren変数に入れます。

返り値を入れておくためのelements変数と、指定されたクラス名にマッチさせるための正規表現patternを用意します。後者は、クラス名が空白で区切られて格納されていることから、/(^|\s)クラス名(\s|$)/という形の正規表現となっています。

あとはchildren中の全ての要素をfor文でループし、その要素のクラス名を取得し、マッチしていればElement.extend()で拡張した上でelementsにpush()します。

ここで、速度をかせぐためにクラス名の長さが0の場合はその場でスキップし、正規表現でチェックする前にその要素にクラスがひとつしかなくそれが該当するクラス名の場合を==演算子でチェックします。最後の手段として正規表現を使って確認しています。

この非XPath版のgetElementsByClassName()は、チェックするHTMLツリーが複雑な場合、かなりの時間がかかってしまう場合があるので注意してください。

おすすめ記事

記事・ニュース一覧