prototype.jsを読み解く

第8回 Prototypeライブラリ(2277~2620行目)

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

ユニットテスト

PrototypeライブラリをSubversionからcheckoutすると,test/ ディレクトリ以下にユニットテスト用のファイルが含まれています。

今回ライブラリ内に疑問点などがあったり,こういうCSSセレクタで正しく動くのだろうか,というのを確認するのに便利に使いました。

assertEnumEqual([], $$('#p + #p'));

というような行を追加するだけで、ブラウザから簡単にテストが行えます。

では,Selectorクラスの後半です。

2277:   handlers: {
2278:     // UTILITY FUNCTIONS
2279:     // joins two collections
2280:     concat: function(a, b) {
2281:       for (var i = 0, node; node = b[i]; i++)
2282:         a.push(node);
2283:       return a;
2284:     },
2285: 
2286:     // marks an array of nodes for counting
2287:     mark: function(nodes) {
2288:       for (var i = 0, node; node = nodes[i]; i++)
2289:         node._counted = true;
2290:       return nodes;
2291:     },
2292: 
2293:     unmark: function(nodes) {
2294:       for (var i = 0, node; node = nodes[i]; i++)
2295:         node._counted = undefined;
2296:       return nodes;
2297:     },
2298: 

2277行目からは,様々な処理を詰め込んだSelector.handlersです。まずは内部で使われるユーティリティ関数です。

concat()は,ノード配列aにbをマージするだけです。aの後ろにbが連結された配列を返します。

mark(), unmark() は,渡されたノード配列の中身各々について,_countedプロパティをtrueに設定したりundefinedにしたりします。両方とも元々の配列(ただし_countedプロパティが操作されたもの)を返します。これは,CSSルールによるフィルタを行う際に,

  1. 基準となるノード配列全体をunmark()する
  2. ルールに該当する要素を抽出する
  3. 抽出された配列をmark()する
  4. 必要に応じて2, 3を繰り返す
  5. 基準となるノード配列から_countedにマークされたものだけを抽出する

とするためなどに使われます。これで順番を保ったまま,重複なしで抽出することができます。

2299:     // mark each child node with its position (for nth calls)
2300:     // "ofType" flag indicates whether we're indexing for nth-of-type
2301:     // rather than nth-child
2302:     index: function(parentNode, reverse, ofType) {
2303:       parentNode._counted = true;
2304:       if (reverse) {
2305:         for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
2306:           node = nodes[i];
2307:           if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
2308:         }
2309:       } else {
2310:         for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
2311:           if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
2312:       }
2313:     },
2314: 

2302行目からはindex()関数です。pesudos.nth()関数からのみ呼ばれています。例えばE:nth-child(99)というセレクタが指定されている場合,pseudos.nth()のnodesには要素Eの配列が入っています。このnodesの中身それぞれの.parentNodeがこのindex()関数に渡されてきます。この時reverseが偽,ofTypeも偽だとすると,2310 行目からのループにおいて,nodes = parentNode.childNodes はnth()内のnodesの一要素である要素Eの兄弟要素全体,となります。この兄弟要素全体をループで回し,該当する要素のnodeIndexプロパティに順番となる数字を入れていきます。この時対象はnodeType == 1 (ELEMENT_NODE) で,かつofTypeが偽かnode._countedが真の場合のみになっています。

ofTypeが偽の場合は,nth-child()系の擬似クラスになるので,その場合は必ずnodeIndexに値を代入します。

ofTypeが真の場合はnth-of-type()系の擬似クラスであり,その場合はnth()のnodesは対象となる要素Eのみが含まれる配列となっているはずです。その時にnth()側でmark(nodes)を使って_countedプロパティがチェックされているはずです。これを利用して,自分自身と同じ要素名を持つ要素だけを順番に数える,という処理にしています。

2315:     // filters out duplicates and extends all nodes
2316:     unique: function(nodes) {
2317:       if (nodes.length == 0) return nodes;
2318:       var results = [], n;
2319:       for (var i = 0, l = nodes.length; i < l; i++)
2320:         if (!(n = nodes[i])._counted) {
2321:           n._counted = true;
2322:           results.push(Element.extend(n));
2323:         }
2324:       return Selector.handlers.unmark(results);
2325:     },
2326: 

2316行目からはunique()関数です。

フィルタ処理で蓄積されたノードの配列に対して,重複が無いようにした配列を返します。

これはSelector.findChildElements()か,Selector.compileMatcher()で生成されるmatcher関数の最後から呼ばれます。

_countedを使う関数は使用後にクリアすることになっているので,ここでは自分で_countedを使って初出のものだけをresults配列に積んでいき,最後にunmark()でクリアしています。

2327:     // COMBINATOR FUNCTIONS
2328:     descendant: function(nodes) {
2329:       var h = Selector.handlers;
2330:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2331:         h.concat(results, node.getElementsByTagName('*'));
2332:       return results;
2333:     },
2334: 

2327行目からは"COMBINATOR FUNCTIONS"すなわちCSSセレクタの結合子に対するハンドラ関数です。

descendant()は,"要素A 要素B"という形式の,ある要素より内側にある要素を示すものです。

CSSセレクタのパーサがdescendant combinatorにマッチするタイミングは,上記"要素A 要素B"の間の空白文字の位置となります。このハンドラに到達するタイミングでは,引数のnodesには"要素A"にマッチするものが集められています。ここでdescendant()ハンドラ関数の役割は,次の"要素B"セレクタでフィルタすることができるように(handlers.tagName()で行われます),nodesの内側の要素を列挙することです。

そのための実装は,渡されたノード配列に対して,各々の子供要素をgetElementsByTagName('*')ですべて列挙して,返り値用のresultsに蓄積して返す形となっています。

2335:     child: function(nodes) {
2336:       var h = Selector.handlers;
2337:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2338:         for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
2339:           if (child.nodeType == 1 && child.tagName != '!') results.push(child);
2340:       }
2341:       return results;
2342:     },
2343: 

2335行目からはchild()ハンドラです。

セレクタの表記では"要素A > 要素B"となり,直下の要素を示します。

渡された各ノードに対してchildNodesプロパティの配列をループで取得し,nodeTypeがELEMENT_NODE(1)のものだけを抽出して返しています。

IEではコメントノードのnodeTypeが1 (ELEMENT_NODE)となってしまう場合があるようで,この場合はtagNameが'!'となるのでこれもスキップします。

2338行目のchildrenは定義はされていますが使われていないようです。

2344:     adjacent: function(nodes) {
2345:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2346:         var next = this.nextElementSibling(node);
2347:         if (next) results.push(next);
2348:       }
2349:       return results;
2350:     },
2351: 

2344行目からはadjacent()です。

CSSセレクタでは"要素A + 要素B"とする部分です。隣接するふたつのノードが対象ですので,渡されたノード配列に対してfor文ですべてループし,各々に対してSelector.handlers.nextElementSibling()を呼び出して,要素が返ってくればpush()しています。

これでresultsで返すのは"要素B"にあたるものの集合,となります。

2352:     laterSibling: function(nodes) {
2353:       var h = Selector.handlers;
2354:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2355:         h.concat(results, Element.nextSiblings(node));
2356:       return results;
2357:     },
2358: 

2352行目からはlaterSibling()です。

原典がどこかわかりませんが,"要素A ~ 要素B"という書式で,"要素A"の兄弟要素のうち,順番が"要素A"より後ろとなる兄弟にマッチします。

実装は,渡された各ノードに対して,Element.nextSiblings()を呼び出して,候補となる要素を配列で返してresultsにマージする,という形になっています。

2359:     nextElementSibling: function(node) {
2360:       while (node = node.nextSibling)
2361:               if (node.nodeType == 1) return node;
2362:       return null;
2363:     },
2364: 

2359行目からはnextElementSibling()です。

これは主に他のハンドラ関数から呼ばれるヘルパ関数で,自分の後続兄弟のうち,ELEMENT_NODE(1)のものが見つかったらそれを返す,というものです。

2365:     previousElementSibling: function(node) {
2366:       while (node = node.previousSibling)
2367:         if (node.nodeType == 1) return node;
2368:       return null;
2369:     },
2370: 

previousElementSibling()は nextElementSibling()の逆で,自分の前にある兄弟の内で,最初に見つかった ELEMENT_NODE(1) であるものを返します。

著者プロフィール

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

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

コメント

コメントの記入