28.2.4优化 DOM 交互
实时更新最小化
let list = document.getElementById("myList"),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode('Item ${i}');
}
以上代码向列表中添加了 10 项。每添加 1 项,就会有两次实时更新:一次添加<li>
元素,一次为
它添加文本节点。因为要添加 10 项,所以整个操作总共要执行 20 次实时更新。
为解决这里的性能问题,需要减少实时更新的次数。有两个办法可以实现这一点。第一个办法是从
页面中移除列表,执行更新,然后再把列表插回页面中相同的位置。这个办法并不可取,因为每次更新
时页面都会闪烁。第二个办法是使用文档片段构建 DOM 结构,然后一次性将它添加到 list 元素。这
个办法可以减少实时更新,也可以避免页面闪烁。比如:
let list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
这样修改之后,完成同样的操作只会触发一次实时更新。这是因为更新是在添加完所有列表项之后
一次性完成的。文档片段在这里作为新创建项目的临时占位符。最后,使用 appendChild()将所有项
目都添加到列表中。别忘了,在把文档片段传给 appendChild()时,会把片段的所有子元素添加到父
元素,片段本身不会被添加。
只要是必须更新 DOM,就尽量考虑使用文档片段
来预先构建 DOM 结构,然后再把构建好的 DOM
结构实时更新到文档中。
使用 innerHTML
对于大量 DOM 更新,使用 innerHTML 要比使用标准 DOM 方法创建同样的结构快很多。 前面的例子如果使用 innerHTML 重写就是这样的:
let list = document.getElementById("myList"),
html = "";
for (let i = 0; i < 10; i++) {
html += "<li>Item ${i}</li>";
}
list.innerHTML = html;
以上代码构造了一个 HTML 字符串,然后将它赋值给 list.innerHTML,结果也会创建适当的 DOM 结构。虽然拼接字符串也会有一些性能损耗,但这个技术仍然比执行多次 DOM 操作速度更快。 与其他 DOM 操作一样,使用 innerHTML 的关键在于最小化调用次数。例如,下面的代码使用 innerHTML 的次数就太多了:
let list = document.getElementById("myList");
for (let i = 0; i < 10; i++) {
list.innerHTML += "<li>Item ${i}</li>"; // 不要
}
这里的问题是每次循环都会调用 innerHTML,因此效率极低。事实上,调用 innerHTML 也应该看 成是一次实时更新。构建好字符串然后调用一次 innerHTML 比多次调用 innerHTML 快得多。
注意:使用 innerHTML 可以提升性能,但也会暴露巨大的 XSS 攻击面。无论何时使用它填充不受控的数据,都有可能被攻击者注入可执行代码。此时必须要当心。
使用事件委托
事件委托利用了事件的冒泡。任何冒泡的事件都可以不在事件目标上,而在目标的任何祖先元素上 处理。基于这个认知,可以把事件处理程序添加到负责处理多个目标的高层元素上。只要可能,就应该 在文档级添加事件处理程序,因为在文档级可以处理整个页面的事件。
注意 HTMLCollection
任何时候,只要访问 HTMLCollection,无论是它的属性还是方法,就会触发查询文档,而这个查询相 当耗时。
减少访问 HTMLCollection 的次数可以极大地提升脚本的性能。 可能优化 HTMLCollection 访问最关键地方就是循环了。之前,我们讨论过要把计算 HTMLCollection 长度的代码转移到 for 循环初始化的部分。来看下面的例子:
let images = document.getElementsByTagName("img");
for (let i = 0, len = images.length; i < len; i++) {
// 处理
}
这里的关键是把 length 保存到了 len 变量中,而不是每次都读一次 HTMLCollection 的 length 属性。在循环中使用 HTMLCollection 时,应该首先取得对要使用的元素的引用,如下面所示。这样 才能避免在循环体内多次调用 HTMLCollection:
let images = document.getElementsByTagName("img"),
image;
for (let i = 0, len = images.length; i < len; i++) {
image = images[i];
// 处理
}
这段代码增加了 image 变量,用于保存当前的图片。有了这个局部变量,就不需要在循环中再访 问 images HTMLCollection 了。
编写 JavaScript 代码时,关键是要记住,只要返回 HTMLCollection 对象,就应该尽量不访问它。 以下情形会返回 HTMLCollection:
- 调用 getElementsByTagName();
- 读取元素的 childNodes 属性;
- 读取元素的 attributes 属性;
- 访问特殊集合,如 document.form、document.images 等。 理解什么时候会碰到 HTMLCollection 对象并适当地使用它,有助于明显地提升代码执行速度。