DocumentFragmentの考察

何が出来るのか

最終更新
2002-12-22T20:27:51+09:00

DOMユーザーの方は、このようなことが出来たら良いと思ったことはありませんか?

  • NodeListのクローンを作成する
  • NodeListをそのままappendChildのパラメータにする

もちろんこのようなことは出来ません。NodeListのitem一つ一つのクローンを作成し、一つ一つをappendChildしなければならないのです。しかし、DocumentFragmentを利用することによって、このような感覚の操作をすることが可能になります。

具体的なユースケースの紹介

最終更新
2003-11-30T20:01:23+09:00

ここに、文書A、文書B、文書C があったとします。

文書A
<rootA>
  <item />
  <item />
  <item />
</rootA>
文書B
<rootB />
文書C
<rootC />

ここで、文書Aのルート要素の子供達を、文書Bと文書Cに移植したいとします。これは正にDocumentFragmentを中継すべき操作の一つです。

まず、文書Aのルート要素の子供たちを、DocumentFragmentに格納します。ここで変数docAdocBdocCは、文書AのDocumentオブジェクトです(言語は読みやすいPythonで)。

dfA = docA.createDocumentFragment()
nl = docA.documentElement.childNodes
for elm in nl:
    dfA.appendChild(elm.cloneNode(true))

次に、文書B、文書C にこのDocumentFragmentを移植します。

dfB = docB.importNode(dfA, true)
docB.documentElement.appendChild(dfB)
dfC = docC.importNode(dfA, true)
docC.documentElement.appendChild(dfC)

ちなみにMSXML4.0ではimportNodeメソッドでインポートする必要がありません。そもそもimportNodeというメソッドがあったかどうか。

パフォーマンスの改善を見る実例

最終更新
2002-12-22T20:27:51+09:00

操作する文書の断片

リスト1 リスト2 リスト3 リスト4 リスト5 リスト6 リスト7 リスト8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8
  • 項目1
  • 項目2
  • 項目3
  • 項目4
  • 項目5
  • 項目6
  • 項目7
  • 項目8

次のUL要素のidは「additionalLists」です。

  • 項目9
  • 項目10
  • 項目11
  • 項目12
  • 項目13
  • 項目14
  • 項目15
  • 項目16

操作内容

リスト1 から リスト8 それぞれに、項目9 から 16 を追加します。

function addListsViaDFragment()
{
 var elmUL = document.getElementById('additionalLists');
 var nlListsToAdd = elmUL.getElementsByTagName('LI');
 // 項目9から16のLI要素を含んだNode Listを作成

 var dfLists = document.createDocumentFragment();
 // DocumentFragmentオブジェクトを作成

 for(var i = 0, len = nlListsToAdd.length, elmLI;
  i < len;
  i++)
 {
  elmLI = nlListsToAdd.item(i);
  dfLists.appendChild(elmLI.cloneNode(true));
 }
 // 項目9から16のLI要素の複製をDocumentFragmentの子として追加

 var elmTable = document.getElementsByTagName('TABLE').item(0);
 var nlULs = elmTable.getElementsByTagName('UL');
 // 追加先のUL要素を含んだNode Listを作成

 for(var i = 0, len = nlULs.length, elmUL, dfClone;
  i < len;
  i++)
 {
  elmUL = nlULs.item(i);
  dfClone = dfLists.cloneNode(true);
  elmUL.appendChild( dfClone );
 }
 // 各UL要素に、DocumentFragmentの複製を追加
 // appendChildの引数はDocumentFragmentになっていますが、
 // 実際に追加されるのはDocumentFragmentの子供達だけです。
}

ご説明

  1. 項目9から16のLI要素それぞれの複製を、DocumentFragmentの子供として追加(計8回のappendChild)
  2. DocumentFragmentの複製を作り、リスト1から8の各UL要素に追加(計8回のappendChild)

結果的に、16回のappendChildが行われます。

もしDocumentFragmentを利用しなかったらどうなるか

DocumentFragmentを利用しない場合には結構面倒なことになるのではないでしょうか。

色々な方法がありますが、「Nodeの追加に関してDOMのインターフェイスだけを利用する」という制限を設けると、8*8で計64回はappendChildしなければなりません。

普通はこのような場合innerHTMLを利用することを思いつきます。非常にシンプルなコードで済むからです。しかし敢えてDocumentFragmentを活用するメリットは、もちろん標準に準拠しているということもありますが、処理速度です。

innerHTMLの書き換えは低速です。……と、Dudeさん(誰)が言ってました(参考:更なるパフォーマンス向上のヒント)。

function addListsViaInnerHTML()
{
 var d = document;
 var htmlListsToAdd = d.getElementById('additionalLists').innerHTML;
 var elmTable = d.getElementsByTagName('TABLE').item(0);
 var nlULs = elmTable.getElementsByTagName('UL');
 for(var i = 0, len = nlULs.length, elmUL;
  i < len;
  i++)
 {
  elmUL = nlULs.item(i);
  elmUL.innerHTML += htmlListsToAdd;
 }
}

ちょっと体感できませんか。ではとりあえず25回やってみましょう。(ご注意:Geckoエンジンの場合innerHTMLの書き換えが極めて低速ですので、次のボタンは押さない方が良いと思います

いやー凄い差がでるものですね。私の環境(IE6/Win)では、DocumentFragmentで3秒弱、innerHTMLで10秒近くかかりました。やっぱりDudeさん(誰)の言うことは聞くべきです。

もちろん項目9から16を一つずつコピーして追加する方法も低速です。

function addLists()
{
 var d = document;
 var elmUL = d.getElementById('additionalLists');
 var nlListsToAdd = elmUL.getElementsByTagName('LI');
 var elmTable = d.getElementsByTagName('TABLE').item(0);
 var nlULs = elmTable.getElementsByTagName('UL');
 for(var i = 0, len = nlListsToAdd.length, elmLI;
  i < len;
  i++)
 {
  elmLI = nlListsToAdd.item(i);
  for(var j = 0, jlen = nlULs.length, elmUL, elmLIClone;
   j < jlen;
   j++)
 {
   elmUL = nlULs.item(j);
   elmLIClone = elmLI.cloneNode(true);
   elmUL.appendChild(elmLIClone);
 }
 }
}

IEではinnerHTMLより遅いかも知れません。但しGeckoエンジンの場合はinnerHTMLよりは速いです。

まとめ

  • 木構造をしていない文書の断片を切り貼りするような操作では、DocumentFragmentを活用しましょう。
  • innerHTMLプロパティの書き込みは、できるだけ行わないようにしましょう。わらい。

パフォーマンスの改善を見る実例 - 追記

最終更新
2005-03-23T09:20:30+09:00

前回、8個の項目を持ったリスト(UL要素)に、更に8個の項目を加えるという作業を行いました。しかし、貼り付ける(appendChildする)際にはDocumentFragmentのお陰で一回で済みましたが、切り取る際には8回行わなければなりませんでした。今回は、DOM Rangeインターフェイスを利用して、切り取りと貼りつけを一回ずつで済ます方法を紹介します。2005-03-23現在、Mozilla系のブラウザ以外では出来ません。

HTML文書断片
<ul id="oldList">
	<li>項目1</li>
	<li>項目2</li>
	<li>項目3</li>
	<li>項目4</li>
	<li>項目5</li>
	<li>項目6</li>
	<li>項目7</li>
	<li>項目8</li>
</ul>

<ul id="newList">
	<li>項目9</li>
	<li>項目10</li>
	<li>項目11</li>
	<li>項目12</li>
	<li>項目13</li>
	<li>項目14</li>
	<li>項目15</li>
	<li>項目16</li>
</ul>
JavaScriptコード
var df,
    oldList = document.getElementById("oldList"),
    newList = document.getElementById("newList"),
    range = document.createRange();                 // (1)

range.selectNodeContents(newList);                  // (2)
df = range.extractContents()                        // (3)
oldList.appendChild(df);                            // (4)
range.selectNode(newList);                          // (5)
range.deleteContents();                             // (6)

(1) 「切り取り貼り付け用選択マシン(違)」を作成します。Rangeオブジェクトです。

(2) 追加するリスト項目(LI要素達)を選択します。このとき、UL要素の中身を選択するのであって、UL要素自身は選択しないので、selectNodeContentsメソッドを使います。イメージとしては、UL開始タグの直後から、UL終了タグの直前までを選択するといったところです。

(3) です。ここでは(2)で選択したLI要素達を切り取っています。切り取った複数のLI要素は新しいDocumentFragmentに格納されます。ここをきちんと把握する為にdfという変数に格納していますが、必要ありません。

(4) では切り取ったLI要素達(DocumentFragment)を古いUL要素に追加しています。DocumentFragmentの中身だけがUL要素に追加され、DocumentFragmentはカラッポになります。

(5)、(6) では、中身を切り取られてカラッポになったUL要素を削除しています。removeChildメソッドを使用しても良いのですが、せっかくRangeオブジェクトを作成したのですから、Rangeオブジェクトに削除させましょう。parentNodeに一々アクセスせずに済むので、私はこの方が好きです。興味ある方は(4)もRangeオブジェクトのメソッドで置き換えてみてください。

Rangeオブジェクトを使用すると、内部的にDocumentFragmentを多用していることが分かります。DocumentFragmentを知らなくても操作は十分に可能ですが、知らなければ(4)のような仕様を跨いだ操作はしにくいでしょう。

DocumentFragmentの特徴

最終更新
2002-12-22T20:27:51+09:00

インターフェイス

DocumentFragmentは、Nodeインターフェイスを継承し、独自のインターフェイスを持ちません。その名前に反して、Documentインターフェイスは継承しませんので注意が必要です。

継承は形式的なものです。たとえば、Nodeインターフェイスが実装しているhasAttributesメソッドも利用できますが、その結果は必ずfalseになります。attributesは、必ずnullです。つまりDocumentFragmantは、属性を持ちません

appendChild等のパラメータに指定した際の挙動

DocumentFragmentをappendChildやinsertBeforeメソッドのパラメータに指定すると、DocumentFragmentの中身(子要素)が全て移動しますが、自分自身はツリーに追加されません。その後DocumentFragmentのchildNodesを参照すると、空っぽになっていることが分かります。

特徴まとめ

DocumentFragmentは、自分自身は決してDOMツリーに追加されることのない、Nodeを格納する容器のようなものです。