JavaScript系统学习DOM系列文章之DocumentFragment

引言:为什么会把DocumentFragment与前端性能扯到一起,这是本篇文章给你的答案。

我们经常使用javascript来操作DOM元素,比如使用appendChild()方法。每次调用该方法时,浏览器都会重新渲染页面。如果大量的更新DOM节点,则会非常消耗性能,影响用户体验。

javascript提供了一个文档片段DocumentFragment的机制。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点。把所有要构造的节点都放在文档片段中执行,这样可以不影响文档树,也就不会造成页面渲染。当节点都构造完成后,再将文档片段对象添加到页面中,这时所有的节点都会一次性渲染出来,这样就能减少浏览器负担,提高页面渲染速度。

接下来,我们先认识下DocumentFragment的API,才能更好的帮助我们理解。

DocumentFragment类型

创建一个DocumentFragment类型的节点可以使用以下两种方法:

// 使用Document对象创建
var dfragment1 = document.createDocumentFragment();
// 使用构造函数创建
var dfragment2 = new DocumentFragment();

一个DocumentFragment类型的节点具有以下特征:

  • nodeType值为11
  • nodeName值为"#document-fragment"
  • nodeValue值为null
  • parentNode值为null
  • 子节点可以是:Element、Comment、Text、ProcessingInstruction、CDATASection、EntityReference

不能直接把DocumentFragment类型添加到文档中,但可以将它做来一个仓库,用来存储可能会被添加到文档中的节点。

DocumentFragment类型介绍

DocumentFragment对象继承自Node,Node对象中所有的方法和属性都可以在DocumentFragment中使用,但DocumentFragment对象通常用于执行那些针对DOM的操作。

在浏览器所使用的一般是html或xhtml类型的文档,所以entities和notations属性都是一个空列表。

如,向以下ul列表表中插入子节点:

<ul id="myUl"></ul>

代码如下:

不使用DocumentFragment

var ul = document.getElementById("ul");
for (var i = 0; i < 20; i++) {
 var li = document.createElement("li");
 li.innerHTML = "index: " + i;
 ul.appendChild(li);
}

使用DocumentFragment

var dfragment = document.createDocumentFragment();
var ul = document.getElementById('myUl');
var li = null;
for(var i=0; i<3; i++){
 li = document.createElement('li');
 li.appendChild(document.createTextNode('列表项:'+i));
 dfragment.appendChild(li);
}
ul.appendChild(dfragment);

由于每一次对文档的插入都会引起重新渲染(计算元素的尺寸,显示背景,内容等),所以进行多次插入操作使得浏览器发生了很多次渲染,方式一效率是比较低的。这是我们提倡通过减少页面的渲染来提高DOM操作的效率的原因。

一个优化的方法是将要创建的元素写到一个字符串上,然后一次性写到innerHTML上,这种利用浏览器对innerHTML的解析确实是相比上面的多次插入快了很多。但是构造字符串灵活性上面比较差,很难符合创建各种各样的DOM元素的需求

利用DocumentFragment,可以弥补这两个方法的不足。

DocumentFragment是没有父节点最小的文档对象,用于存储HTML和XML片段。DocumentFragment对象继承Node,所以它有Node的所有属性方法,完全可以操作Node(NodeList)那样操作DocumentFragment。此外W3C对DocumentFragment也定义了一些另外的属性和方法,但是由于多数浏览器都没有实现,从兼容性上来说不推荐使用这些属性。具体有哪些属性方法可以参考MDN说明。

创建DocumentFragment的方法有两种,document.createDocumentFragment()和new Fragment()。对于document.createDocumentFragment(),所有浏览器都支持(包括IE6),而构造函数方法就不是所有浏览器都有效了(IE没有实现该方法)。所以从兼容性上来说推荐使用document.createDocumentFragment()

上面也提到,使用DocumentFragment与一般的Node无异,可以当作是DOM对象一样操作。在使用appendChild,insertBefore等方法时,被添加(插入)的是片段的所有子节点,而非本身。

因为文档片段存在于内存中,并不在DOM中,所以将子元素插入到文档片段中时不会引起页面回流(对元素位置和几何上的计算),因此使用DocumentFragment可以起到性能优化的作用。

为什么推出DocumentFragment,我总结了DocumentFragment特殊之处:

1、DocumentFragment它并不属于DOM结构的一部分,所以任何对DocumentFragment的操作,不会影响到DOM

2、当我们给appendChild(), insertBefore(), replaceChild()等传入一个DocumentFragment的时候,是把DocumentFragment的所有子节点一次性地插入,而不是DocumentFragment本身

3、当我们把DocumentFragment插入到别的节点之后,DocumentFragment的子节点会自动被清空。

4、当我们把现有的DOM上的一个节点插入给DocumentFragment,这个节点会从原DOM上被删掉。

第一点和第二点主要是讲利用DocumentFragment来进行DOM操作在安全性和性能方面的优点。第三点也是它作为临时容器的一个优点,用完之后呢,会自己清空自己,不占内存。特别要注意的是第四点,还挺出乎我意料的,我就第四点来做一个实验:

<ul class='bookList'>
 <li class='bookItem'>book 1</li>
 <li class='bookItem'>book 2</li>
</ul> 

我们创建一个DocumentFragment,然后把第一个'<li>'给它做子节点:

var bookList = document.getElementsByClassName('bookList')[0];
var firstLi = bookList.firstElementChild;
var df = document.createDocumentFragment();
df.appendChild(firstLi);

在执行了上面的代码之后,原来的HTML就变成了:

<ul class="bookList">
 
 <li class="bookItem">book 2</li>
</ul>

可以看到,我们把第一个'<li>'插入到了我们创建的DocumentFragment里面, 然后这个'<li>'就从原来的DOM里面消失了

写在最后

如果你正在学习JavaScript DOM这里,一定要仔细的看看这里,因为大公司的面试题里也藏着这道经典的题,如何优化大量dom操作?同时也有很多人疑问DocumentFragment 真的能提高 JS 动态添加 DOM 的性能吗?不要只道听途书,要知道具体为什么。

前端学习视频传送门

vue视频教程来啦,每周末都有前端视频教程学

微信小程序视频教程来啦,每周末都有前端视频教程学

vue视频教程来啦,每周末送前端视频教程,私信可得

从入门到精通实战Go web编程视频来啦,每周末都有前端视频教程学

举报
评论 0