DOMユーザーの方は、このようなことが出来たら良いと思ったことはありませんか?
もちろんこのようなことは出来ません。NodeListのitem一つ一つのクローンを作成し、一つ一つをappendChildしなければならないのです。しかし、DocumentFragmentを利用することによって、このような感覚の操作をすることが可能になります。
ここに、文書A、文書B、文書C があったとします。
<rootA>
<item />
<item />
<item />
</rootA>
<rootB />
<rootC />
ここで、文書Aのルート要素の子供達を、文書Bと文書Cに移植したいとします。これは正にDocumentFragmentを中継すべき操作の一つです。
まず、文書Aのルート要素の子供たちを、DocumentFragmentに格納します。ここで変数docA、docB、docCは、文書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というメソッドがあったかどうか。
| リスト1 | リスト2 | リスト3 | リスト4 | リスト5 | リスト6 | リスト7 | リスト8 |
|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
次のUL要素のidは「additionalLists」です。
リスト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の子供達だけです。
}
結果的に、16回のappendChildが行われます。
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よりは速いです。
前回、8個の項目を持ったリスト(UL要素)に、更に8個の項目を加えるという作業を行いました。しかし、貼り付ける(appendChildする)際にはDocumentFragmentのお陰で一回で済みましたが、切り取る際には8回行わなければなりませんでした。今回は、DOM Rangeインターフェイスを利用して、切り取りと貼りつけを一回ずつで済ます方法を紹介します。2005-03-23現在、Mozilla系のブラウザ以外では出来ません。
<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>
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は、Nodeインターフェイスを継承し、独自のインターフェイスを持ちません。その名前に反して、Documentインターフェイスは継承しませんので注意が必要です。
継承は形式的なものです。たとえば、Nodeインターフェイスが実装しているhasAttributesメソッドも利用できますが、その結果は必ずfalseになります。attributesは、必ずnullです。つまりDocumentFragmantは、属性を持ちません。
DocumentFragmentをappendChildやinsertBeforeメソッドのパラメータに指定すると、DocumentFragmentの中身(子要素)が全て移動しますが、自分自身はツリーに追加されません。その後DocumentFragmentのchildNodesを参照すると、空っぽになっていることが分かります。
DocumentFragmentは、自分自身は決してDOMツリーに追加されることのない、Nodeを格納する容器のようなものです。