JavaScript笔记 | 如何高效操作DOM

546 阅读3分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

写在前面

在了解如何操作DOM之前,我们先来了解下DOM的概念。DOM(Document Object Modal,文档对象模型),是JavaScript操作HTML的接口。

DOM主要由3个部分组成:

  • DOM节点
  • DOM事件
  • 选择区域 (选择区域的使用场景有限,一般用于富文本编辑类业务)

注意区分:标签是HTML的基本单位,比如;p、div、input;节点是DOM的基本单位,有多种类型,比如注释节点、文本节点;元素是节点的一种,与HTML标签相对应,比如p标签会对应p元素。

为什么DOM操作耗时

浏览器的工作机制

浏览器包含渲染引擎和JavaScript引擎,他们属于单线程运行。单线程优势是开发方便,避免多线程下的死锁、竞争等问题,劣势是失去了并发能力。

每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。如果频繁地大量切换,那么性能问题也就逐渐暴露了。

如果在循环中包含一些复杂的逻辑或者说涉及到多个元素时,就会造成不可忽视的性能损耗。

重新渲染 (重排和重绘)

浏览器在渲染页面时,如果在操作DOM时涉及到元素、样式的修改,就会引起渲染引擎重新计算样式生成css树,同时还有可能触发对元素的重新排布(简称“重排”)和重新绘制(简称“重绘”)。

  • 影响到其他元素排布的操作就会引起重排,继而引发重绘。比如:

    • 修改元素边距、大小
    • 添加、删除元素
    • 改变窗口大小
  • 引起重绘。比如:

    • 设置背景图片
    • 修改字体颜色
    • 改变 visibility属性值

重排渲染耗时明显高于重绘,重排会导致重绘。

如何高效操作DOM

在循环外操作元素

循环10000次DOM元素和循环10000次JSON对象,所耗时间相差一个量级。

    const times = 10000;
    // 循环DOM
    console.time('dom');
    for(let i = 0; i < times; i++) {
        document.body === '' ? void 0 : void 0;
    }
    console.timeEnd('dom'); // dom: 0.955322265625 ms

    // 循环JSON
    var body = JSON.stringify(document.body);
    console.time('json');
    for(let i = 0; i < times; i++) {
        body === '' ? void 0 : void 0;
    }
    console.timeEnd('json'); // json: 0.56884765625 ms

即使在循环外也要尽量减少操作元素,因为不知道他人调用你的代码时是否处于循环中。

批量操作元素

示例:要在页面上插入1 万个 div 元素,在循环中直接创建再添加到父元素上耗时会非常多

    // 方法1
    const times = 10000;
    console.time('createElement');
    for(let i = 0; i<times; i++) {
        const div = document.createElement('div');
        document.body.appendChild(div);
    }
    console.timeEnd('createElement'); // createElement: 39.69921875 ms
    
    // 方法2
    const times = 10000;
    console.time('innerHTML');
    let html = '';
    for (let i=0;i<times;i++) {
        html += '<div></div>'
    }
    document.body.innerHTML += html;
    console.timeEnd('innerHTML'); // innerHTML: 0.683837890625 ms

虽然通过修改 innerHTML 来实现批量操作的方式效率很高,但它并不是万能的。比如要在此基础上实现事件监听就会略微麻烦,只能通过事件代理或者重新选取元素再进行单独绑定。

缓存元素集合

将通过选择器函数获取到的 DOM 元素赋值给变量,之后通过变量操作而不是再次使用选择器函数来获取。

// 不推荐写法
for (let i = 0; i < document.querySelectorAll('div').length; i++) {
  document.querySelectorAll(`div`)[i].innerText = i
}

// 推荐写法(将元素集合赋值给 JavaScript 变量,每次通过变量去修改元素,那么性能将会得到不小的提升。)
const divs = document.querySelectorAll('div')
for (let i = 0; i < divs.length; i++) {
  divs[i].innerText = i
}

总结

本篇分析过程:深入理解DOM的必要性=>分析DOM操作耗时的原因=>提出解决办法

关于提升渲染性能,拓展的相关方法:

  • 尽量不要使用复杂的匹配规则和复杂的样式,从而减少渲染引擎计算样式规则生成CSSDOM树的时间。

  • 尽量减少重排、重绘影响的区域。

  • 使用css3特性来实现动画效果。