prototype.jsを読み解く

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

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

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ツリーが複雑な場合,かなりの時間がかかってしまう場合があるので注意してください。

著者プロフィール

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

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