Jintrick.netagenda2008年02月アーカイブ → 2008年02月20日

文書ツリーを多数回変更するときはDocument Fragmentを経由する

「文書ツリーを多数回変更するときは、直接行なわずにDocument Fragmentを経由してくれ」という話。

一度DOMの実装を書いてみるなり、書こうとしてみるなりすれば分かる。DOM文書ツリーの変更、特にHTML文書におけるそれがどれだけの要素に影響を与えるかが。Document Fragmentの何が良いかといえば、例えば「View」から切り離されていることが挙げられると思う。DOM実装に依存するし想像でしかないけれども、文書ツリーに直接何らかのノードを加えたり削除したりすると、表示スタイルの計算にリソースを多少なり消費するが、Document Fragment中のノード群を操作する限りスタイルは関係ない。操作が完了した後に実際の文書ツリーに一回だけ変更を加えるなら、スタイルの計算も一回ですむ。文字列連結を何度も繰り返すか、それとも文字列の配列を一回だけjoinするかの違いを連想してもいい。

私は最近Baidu(百度)とやらをリキッドマルチカラムにする為のGreasemonkeyスクリプトを書くに当たって、URLの折り返し問題を解決しようと思ってFirefoxで長いURIを折り返すという記事を読んだ。どんなロジックかと思って覗いてみると、どうやら文書中のスラッシュ等の文字の後にwbr要素を挿入して折り返しを実現しているのだが、文書ツリーに直接変更を加えている。まさに膨大なRange.insertNodeが文書ツリーに対して繰り返されるわけだ。こういった事例では私なら何よりもまずパフォーマンスに注意をしてスクリプトを書く。ぶっちゃけると、こういうことを平気でやらかすpiro氏のJavascriptアプリケーションを(もちろんXULアプリケーションを含め)、私は到底使う気にはなれない。(※1)

この事例でDocument Fragmentを経由する場合、body要素をDocument Fragment(以下df)内に抽出し、文書操作はそのdfの中で全て行い終えた後、元の文書ツリーに戻してやるという形になる。私は文書を切ったり貼ったりする時にはDOM Rangeを使っている。

コンテクストノードをDocument Fragmentとして抽出するまで
var rngBody = document.createRange();
rngBody.selectNode(document.body);
var df = rngBody.extractContents();

extractContentsメソッドの戻り値は選択した範囲を含んだDocument Fragmentである。

次に、XPathのコンテクストノードをdf内のbody要素にして、子孫のテキストノードに対して処理を行なう。このbody要素はdf直下のただ一つの子どもになっており、df.firstChildで参照できる。Document Fragment自身をコンテクストノードにすることはできないが、Document Fragement中のElementに対してなら可能なようだ。不便。

ともかく、処理を終えたらdfの中身を元の位置にinsertNodeすればいい:

抽出したコンテクストノードを元に戻す
rngBody.insertNode(df);

ちなみにスラッシュを10個含んだspan要素が5000個存在する文書でテストした結果、Document Fragmentを経由させると自分の環境では処理速度が30~35%改善した。まあそれでもこんな処理をするGreasemonkeyスクリプトを「@include *」で使うことはしないし、実際にはコンテクストノード、ロケーションパス、正規表現を引数にとる関数にして部品にして使うけど。

テストした関数
function breakURLString(
    /* コンテクストノード */ contextNode,
    /* テキストノードを表現するXPath */ locText,
    /* 折り返し点となる文字の正規表現 */ reBreaker){
    
    var rngContext = document.createRange();
    rngContext.selectNode(contextNode);
    
    var df = rngContext.extractContents();
    
    contextNode = df.firstChild;
    var nl = document.evaluate(
        locText,
        contextNode,
        document.createNSResolver(contextNode),
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );
    
    var wbr = document.createElement("wbr");
    var rngText = document.createRange();
    var i = nl.snapshotLength;
    var currentNode; // each text node
    var offset; // where wbr element is inserted at
    var cdata; // data attribute of current text node
    while(i--) {
        currentNode = nl.snapshotItem(i);
        for (;;currentNode = wbr.nextSibling)
        {
            cdata = currentNode.data
            offset = cdata.search(reBreaker)+1;
            if (offset == 0)  break;
            rngText.selectNode( currentNode.splitText(offset) );
            rngText.insertNode( wbr = wbr.cloneNode(false) );
            if (offset == cdata.length) break;
        }
    }
    rngContext.insertNode(df);
}

Javascript1.7が使えないって馬鹿げてる。

※1 追記

話題は逸れるが、この感想文、つまり私が「こういうことを平気でやらかすpiro氏のJavascriptアプリケーションを(もちろんXULアプリケーションを含め)、私は到底使う気にはなれない」と思っていることに関しては本人として異論があったようである。

1 については長くなったのでRe: 僕があまりDocumentFragmentを使っていない理由 (agenda)で述べた。

2については情けないと思う。思えば対照的な出来事があった。数年前、私は某氏のサイトのスクリプトに「いちゃもん」をつけた。彼はすぐに対応してくれたが、後にそのスクリプトは本人の制作物ではなく、完全に他者の者を借りていただけだったことを知った。最後まで黙っていたわけだな。まあそこまでの「男気」みたいなものを期待してはいないし、どうでもいいや。

3。元々そんなもんだろうと思っていた。ただ、以前ツリー型タブ等を導入してみたときには「動作」すらしなかった。その辺の偶々起こった経験にも影響されているかもしれないな。なにせ当時の呼び名がPhoenixだったかFirebirdだったか忘れたが、UIがメチャクチャにぶっ壊れたのは印象的だった。

4については、「自己責任だから」と理由をつければ、自分のその他の作品を「邪推」されないで済むはずだ、という話。しかしその人の書いたコードが公開されていればそれからスキルを推測するのは当たり前ではないか。

5については信用できない。その理由をRe: 書き捨てたコードの品質や書き捨てるという姿勢について (agenda)に書いたところ、「こんなの、論争でもなんでもなかった。ただのいちゃもん」という「最後っ屁」で締めくくられた。わらい。

まあ数少ないagendaの読者は、昔の私の書いたおぞましいスクリプトを探して晒してみるといいだろう。一緒になって笑ってやるよ。言い訳なんかしないからッ。絶対ッ。


webmaster@jintrick.net
公開: 2008年02月20日
カテゴリ: DOM ,Javascript