DocumentFragment文件碎片(高效批量更新多個節點)

舞動乾坤發表於2019-02-02

一般動態建立html元素都是建立好了直接appendChild()上去,但是如果要新增大量的元素還用這個方法的話就會導致大量的重繪以及迴流,所以需要一個'快取區'來儲存建立的節點,然後再一次性新增到父節點中。這時候DocumentFragment物件就派上用場了。 createDocumentFragment有什麼作用呢?

呼叫多次document.body.append(),每次都要重新整理頁面一次。效率也就大打折扣了,而使用document_createDocumentFragment()建立一個文件碎片,把所有的新結點附加在其上,然後把文件碎片的內容一次性新增到document中,這也就只需要一次頁面重新整理就可。

他支援以下DOM2方法:

a, cloneNode, hasAttributes, hasChildNodes, insertBefore, normalize, removeChild, replaceChild.

也支援以下DOM2屬性:

attributes, childNodes, firstChild, lastChild, localName, namespaceURI, nextSibling, nodeName, nodeType, nodeValue, ownerDocument, parentNode, prefix, previousSibling, textContent.

在《javascript高階程式設計》一書的6.3.5:建立和操作節點一節中,介紹了幾種動態建立html節點的方法,其中有以下幾種常見方法:

crateAttribute(name):用指定名稱name建立特性節點

 createComment(text):建立帶文字text的註釋節點

 createDocumentFragment():建立文件碎片節點

 createElement(tagname):建立標籤名為tagname的節點

 createTextNode(text):建立包含文字text的文字節點
複製程式碼

在所有節點型別中,只有文件片段節點DocumentFragment在文件中沒有對應的標記。DOM規定文件片段(document fragment)是一種“輕量級”的文件,可以包含和控制節點,但不會像完整的文件那樣佔用額外的資源

看下w3c的官方說明:

DocumentFragment 節點不屬於文件樹,繼承的 parentNode 屬性總是 null。

不過它有一種特殊的行為,該行為使得它非常有用,即當請求把一個 DocumentFragment 節點插入文件樹時,插入的不是 DocumentFragment 自身,而是它的所有子孫節點。這使得 DocumentFragment 成了有用的佔位符,暫時存放那些一次插入文件的節點。它還有利於實現文件的剪下、複製和貼上操作。

重點就在於DocumentFragment 節點不屬於文件樹。因此當把建立的節點新增到該物件時,並不會導致頁面的迴流,因此效能就自然上去了。

建立文件片段,要使用document.createDocumentFragment()方法。文件片段繼承了Node的所有方法,通常用於執行那些針對文件的DOM操作

文件片段節點的三個node屬性——nodeType、nodeName、nodeValue分別是11、'#document-fragment'和null,文件片段節點沒有父節點parentNode

<div id="test">JavaScript</div>
  const elementNode = document.getElementById('test')
  const attrNode = elementNode.getAttributeNode('id')
  const textNode = elementNode.firstChild
  console.log(elementNode.nodeType, attrNode.nodeType, textNode.nodeType)   // 1 2 3
複製程式碼
var frag = document.createDocumentFragment();
console.log(frag.nodeType);//11
console.log(frag.nodeValue);//null
console.log(frag.nodeName);//'#document-fragment'
console.log(frag.parentNode);//null
 
var fragment = document.createDocumentFragment();
<script type="text/javascript">
    var pNode,fragment = document.createDocumentFragment();
        
    for(var i=0; i<20; i++){
        pNode = document.createElement('p');
        pNode.innerHTML = i;
        fragment.appendChild(pNode);
    }
    document.body.appendChild(fragment);
    
</script>
複製程式碼

作用

  我們經常使用javascript來操作DOM元素,比如使用appendChild()方法。每次呼叫該方法時,瀏覽器都會重新渲染頁面。如果大量的更新DOM節點,則會非常消耗效能,影響使用者體驗

  javascript提供了一個文件片段DocumentFragment的機制。如果將文件中的節點新增到文件片段中,就會從文件樹中移除該節點。把所有要構造的節點都放在文件片段中執行,這樣可以不影響文件樹,也就不會造成頁面渲染。當節點都構造完成後,再將文件片段物件新增到頁面中,這時所有的節點都會一次性渲染出來,這樣就能減少瀏覽器負擔,提高頁面渲染速度   

<ul id="list1"></ul>
<script>
var list1 = document.getElementById('list1');
console.time("time");
var fragment = document.createDocumentFragment();
for(var i = 0; i < 500000; i++){
    fragment.appendChild(document.createElement('li'));
}
list1.appendChild(fragment);
console.timeEnd('time');
</script>

<ul id="list"></ul>
<script>
var list = document.getElementById('list');
console.time("time");
for(var i = 0; i < 500000; i++){
    list.appendChild(document.createElement('li'));
}
console.timeEnd('time');
</script>
複製程式碼

document.createDocumentFragment()說白了就是為了節約使用DOM。每次JavaScript對DOM的操作都會改變頁面的變現,並重新重新整理整個頁面,從而消耗了大量的時間。為解決這個問題,可以建立一個文件碎片,把所有的新節點附加其上,然後把文件碎片的內容一次性新增到document中。

由於文件片段的優點在IE瀏覽器下並不明顯,反而可能成為多此一舉。所以,該型別的節點並不常用

DEMO

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>

	<body>
		<div id="main1"></div>
		<div id="main2"></div>
	</body>
	<script type="text/javascript">
		var d1 = new Date();
		//建立十個段落,常規的方式
		for(var i = 0; i < 1000; i++) {
			var p = document.createElement("p");
			var oTxt = document.createTextNode("段落" + i);
			p.appendChild(oTxt);
			document.getElementById("main1").appendChild(p);
		}
		var d2 = new Date();
		console.log("第一次建立需要的時間:" + (d2.getTime() - d1.getTime()));

		//使用了createDocumentFragment()的程式
		var d3 = new Date();
		var pFragment = document.createDocumentFragment();
		for(var i = 0; i < 1000; i++) {
			var p = document.createElement("p");
			var oTxt = document.createTextNode("段落" + i);
			p.appendChild(oTxt);
			pFragment.appendChild(p);
		}
		document.getElementById("main2").appendChild(pFragment);
		var d4 = new Date();
		console.log("第2次建立需要的時間:" + (d4.getTime() - d3.getTime()));
	</script>

</html>
複製程式碼
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

<div id="test">Node</div>

<div id="app">
  <ul>
    <li>test1</li>
    <li>test2</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
    <li>test3</li>
  </ul>
</div>



<script type="text/javascript">

  //6. DocumentFragment: 文件碎片(高效批量更新多個節點)
  // document: 對應顯示的頁面, 包含n個elment  一旦更新document內部的某個元素介面更新
  // documentFragment: 記憶體中儲存n個element的容器物件(不與介面關聯), 如果更新framgnet中的某個element, 介面不變
  /*
  <div id="app">
    <ul>
      <li>test1</li>
      <li>test2</li>
      <li>test3</li>
      <li>test3</li>
      <li>test3</li>
      <li>test3</li>
      <li>test3</li>
      <li>test3</li>
    </ul>
  </div>
   */
  const div = document.getElementById('app')
  // 1. 建立fragment
  const fragment = document.createDocumentFragment()
  // 2. 取出ul中所有子節點取出儲存到fragment
  let child
  while(child=div.firstChild) { // 一個節點只能有一個父親
    fragment.appendChild(child)  // 先將child從ul中移除, 新增為fragment子節點
  }

  // 3. 更新fragment中所有li的文字
  Array.prototype.slice.call(fragment.childNodes).forEach(node => {
    if (node.nodeType===1) { // 元素節點 <ul>
      Array.from(node.children).forEach(li => li.textContent = '哈哈')
    }
  })

  // 4. 將fragment插入ul
  div.appendChild(fragment)

</script>
</body>
</html>
複製程式碼

相關文章