uhyohyo.net

JavaScript初級者から中級者になろう

七章第四回 ノードをまとめて扱う: DocumentFragment

今回解説するのは、DocumentFragmentというものです。これは、題名の通り、複数のノードをまとめて扱うのに必要なものです。

実はこれはノードの一種です。つまり、「テキストノード」とか「要素ノード」とかと同じように、「DocumentFragment」という種類のノードがあるのです。

ノードの一種だから、appendChildなどで子ノードを追加したり、あるいはcloneNodeでDocumentFragment自身をコピーしたりもできます。

このDocumentFragmentはどういった意味を持つノードなのかということですが、仕様書にはDocumentFragment is a "lightweight" or "minimal" Document object.と書いてあります。これは、簡単に訳すと「小型のdocumentのようなものである」と書いてあります。

つまり、DocumentFragmentは、ひとつの独立した木構造の頂点のノードであるといえます。今実際に画面に表示されている文書とは別に、独自に木構造を作ることができるということです。

しかし、例えばp要素を作ってその子としてテキストノードを作って追加した場合、

 p
|
└――#text

という独立した木構造が生まれます。確かにこれもp要素を頂点とする独立した木構造だといえるのですが、このように要素を頂点とした木構造では表現できない場合があります。例えば、

 A
|
├―― B
|
├―― C
|
├―― D
|
└―― E
という木構造から

 A
|
├―― B
|
├―― C
|
├―― D
|
└―― E

のB,C,Dの部分を抜き出したいとします。もとの木構造は

 A
|
└―― E

となり、抜き出した部分は

├―― B
|
├―― C
|
└―― D

となります。ここで、この3つは確かに兄弟ですが、しかし親がいません。木構造の場合、親がないと兄弟として成り立たないのです。

こういう場合に、仮の親としてこのB,C,Dの木構造の頂点に位置するべきものがDocumentFragmentなのです。つまり、

DocumentFragment
|
├―― B
|
├―― C
|
└―― D

ということになります。

また、もともとある文書から抜き出したものでなくても、新しく作った木構造でも構いません。まず、新しくDocumentFragmentを作るサンプルを見てみます。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>

    <p>test</p>

    <script type="text/javascript">
      var df = document.createDocumentFragment();

      var p1 = document.createElement("p");
      p1.appendChild(document.createTextNode('p1'));
      var div1 = document.createElement("div");
      div1.appendChild(document.createTextNode('div1'));
      var div2 = document.createElement("div");
      div2.appendChild(document.createTextNode('div2'));

      df.appendChild(p1);
      df.appendChild(div1);
      df.appendChild(div2);
    </script>
  </body>
</html>

ここで新登場したcreateDocumentFragmentはdocumentのメソッドで、このようにすることで新しいDocumentFragmentが作れます。返り値が新しくできたDocumentFragmentです。

そして、できたDocumentFragmentにp1,div1,div2の3つの子ノードを追加しています。

DocumentFragment
|
├―― p (p1)
|
├――div (div1)
|
└――div (div2)

という木構造です。

DocumentFragmentの使い方

さて、このDocumentFragmentがどんなものかは分かったと思います。それでは、これが具体的にどんなことに活躍するのかを見ていきたいと思います。

実は、DocumentFragmentには特別な特徴があります。それは、DocumentFragmentをappendChildなどで他のノードの子ノードとして追加しようとしたときに現れます。

上で作ったDocumentFragmentをbody要素に追加してみましょう。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>

    <p>test</p>

    <script type="text/javascript">
      var df = document.createDocumentFragment();

      var p1 = document.createElement("p");
      p1.appendChild(document.createTextNode('p1'));
      var div1 = document.createElement("div");
      div1.appendChild(document.createTextNode('div1'));
      var div2 = document.createElement("div");
      div2.appendChild(document.createTextNode('div2'));

      df.appendChild(p1);
      df.appendChild(div1);
      df.appendChild(div2);

      document.body.appendChild(df);
    </script>
  </body>
</html>

イメージとしてはこんな感じです。

body
|
├―― p (test)
|
├―― script
|
└――DocumentFragment
    |
    ├―― p (p1)
    |
    ├―― div (div1)
    |
    └―― div (div2)

ところが、実際は違うのです。このようにDocumentFragmentを追加した場合、DocumentFragment自体は追加されずDocumentFragmentの子ノードが直接追加されるのです。つまり、

body
|
├―― p (test)
|
├―― script
|
├―― p (p1)
|
├―― div (div1)
|
└―― div (div2)

のようになるのです。

DocumentFragmentを使うことの利点は、例えば画面に影響する木構造へのappendChild(など)を少なくできるという点があります。画面に影響する木構造とは、実際に画面に表示されているものの木構造、つまりdocumentを頂点とする木構造です。これにノードが追加されたりすると、その結果を画面に反映させなければいけません。これは何度もやるのは結構大変な作業です。特に多くのノードを追加したい場合、appendChildをするたびに画面を書き直す必要があり、大変です(ブラウザによっては賢く画面を書きなおすのをサボるものもあります)。そこで、DocumentFragmentに先にまとめて追加しておき(DocumentFragmentは独立した木構造だから追加しても何も起こりません)、最後にDocumentFragmentを追加すればいいのです。そうすれは、画面の木構造はDocumentFragmentを追加したときの1回だけ変化し、描き直しも1回しか起こりません。

また、同じノードの組をたくさん追加したいときなども役立ちます。例えば、

<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>

を全てのul要素に追加したいとします。そうすると、DocumentFragmentを使わずに実現する場合、li要素が5個だからul要素1つにつき5回のappendChildが必要になり、ul要素が5個あれば25回ものappendChildをすることになってしまいます。DocumentFragmentがあれば、次のようにできます。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>

    <ul>
      <li>あ</li>
      <li>い</li>
      <li>う</li>
    </ul>

    <ul>
      <li>イ</li>
      <li>ロ</li>
      <li>ハ</li>
    </ul>

    <ul>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>


    <script type="text/javascript">
      var df = document.createDocumentFragment();	//新しいDocumentFragmentを作る

      //5個のli要素を作ってdfに追加
      for(var i=1;i<=5;i++){
        var li = document.createElement('li');		//li要素を作って
        li.appendChild(document.createTextNode(i));	//テキストノードも作って
        df.appendChild(li);				//dfに追加
      }

      //全てのul要素に対して処理
      var uls = document.getElementsByTagName('ul');
      for(i=0;i<uls.length;i++){
        var ul = uls.item(i);		//処理するul要素
        ul.appendChild( df.cloneNode(true) );	//原本のDocumentFragmentは使いまわせないから、同じものを作って追加
      }
    </script>
  </body>
</html>

こうすれば、画面に影響するappendChild(画面上のul要素に対するappendChild)は各ul要素につき1回となります。