uhyohyo.net

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

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

このページの最終更新日:

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

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

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

このDocumentFragmentはどういった意味を持つノードなのかということですが、典型的な説明としては「小型のdocumentのようなものである」とされます。

つまり、DocumentFragmentは、ひとつの独立した木構造の頂点のノードであるといえます。

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

p
  • #text

という独立した木構造が生まれるのはご存知の通りです。p要素が例えばbodyにappendChildされて初めてこの木構造は文書の一部となり、それまでは文書の木構造とは独立した木構造として存在することになります。なので、独立した木構造を作りたいだけならばDocumentFragmentは必ずしも必要ではないように思えます。しかし、このように要素を頂点とした木構造では表現できない場合があります。例えば、

A
  • B
  • C
  • D
  • E

という木構造からB, C, Dを抜き出したいとします。このとき、もとの木構造は

A
  • E

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

  • B
  • C
  • D

となります。この抜き出した部分が問題で、この3つは兄弟でなければいけませんが、しかし親がいません。木構造の場合、親がないと兄弟として成り立たないのです。

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

DocumentFragment
  • B
  • C
  • D

という木構造ならばよいということです。このように、DocumentFragmentの主な役目は親がいないが兄弟関係にある複数のノードたちに対してその仮の親となることです。(兄弟関係にある)ノードたちをまとめておくための入れ物という見方もできるでしょう。

では、まず新しい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);

      console.log(df);
    </script>
  </body>
</html>

ここで新登場したcreateDocumentFragmentはdocumentのメソッドで、このようにすることで新しいDocumentFragmentが作れます。返り値が新しくできたDocumentFragmentです。当然ながら、新しく作られたDocumentFragmentは中身が空です。子はいません。

そして、できたDocumentFragmentにp1,div1,div2の3つの子ノードを追加しています。console.logでDocumentFragmentを表示すると、それが

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があくまでノードを入れる入れ物であると考えれば納得がいきます。

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

以上がDocumentFragmentの紹介です。使いどころがたまにあるノードの種類です。また、HTML5ではtemplate要素の中身がDocumentFragmentにより表されています(これについてはまた今度の機会に紹介します)。