如何高效操作 DOM 元素?


如何高效操作 DOM 元素?

文章插图

什么是 DOM
DOM(Document Object Model,文档对象模型)是 JAVAScript 操作 html 的接口(这里只讨论属于前端范畴的 HTML DOM),属于前端的入门知识,同样也是核心内容,因为大部分前端功能都需要借助 DOM 来实现,比如:
动态渲染列表、表格表单数据;
监听点击、提交事件;
懒加载一些脚本或样式文件;
实现动态展开树组件,表单组件级联等这类复杂的操作 。
如果你查看过 DOM V3 标准,会发现包含多个内容,但归纳起来常用的主要由 3 个部分组成:
DOM 节点
DOM 事件
选择区域
选择区域的使用场景有限,一般用于富文本编辑类业务,我们不做深入讨论;DOM 事件有一定的关联性,将在下一课时中详细讨论;对于 DOM 节点,需与另外两个概念标签和元素进行区分:
标签是 HTML 的基本单位,比如 p、div、input;
节点是 DOM 树的基本单位,有多种类型,比如注释节点、文本节点;
元素是节点中的一种,与 HTML 标签相对应,比如 p 标签会对应 p 元素 。
举例说明,在下面的代码中,“p” 是标签,生成 DOM 树的时候会产生两个节点,一个是元素节点 p,另一个是字符串为“亚里士朱德”的文本节点 。
复制<p>亚里士朱德</p>
会框架更要会 DOM
有的前端工程师因为平常使用 Vue、React 这些框架比较多,觉得直接操作 DOM 的情况比较少,认为熟悉框架就行,不需要详细了解 DOM 。这个观点对于初级工程师而言确实如此,能用框架写页面就算合格 。
但对于屏幕前想成为高级/资深前端工程师的你而言,只会使用某个框架或者能答出 DOM 相关面试题,这些肯定是不够的 。恰恰相反,作为高级/资深前端工程师,不仅应该对 DOM 有深入的理解,还应该能够借此开发框架插件、修改框架甚至能写出自己的框架 。
因此,这一课时我们就深入了解 DOM,谈谈如何高效地操作 DOM 。
为什么说 DOM 操作耗时
要解释 DOM 操作带来的性能问题,我们不得不提一下浏览器的工作机制 。
线程切换
如果你对浏览器结构有一定了解,就会知道浏览器包含渲染引擎(也称浏览器内核)和 JavaScript 引擎,它们都是单线程运行 。单线程的优势是开发方便,避免多线程下的死锁、竞争等问题,劣势是失去了并发能力 。
浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞 。操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换 。而这个操作相对而言是比较耗时的 。
每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗 。单次切换消耗的时间是非常少的,但是如果频繁的大量切换,那么就会产生性能问题 。
比如下面的测试代码,循环读取一百万次 DOM 中的 body 元素的耗时是读取 JSON 对象耗时的 10 倍 。
// 测试次数:一百万次const times = 1000000// 缓存body元素console.time('object')let body = document.body// 循环赋值对象作为对照参考for(let i=0;i<times;i++) {let tmp = body}console.timeEnd('object')// object: 1.77197265625msconsole.time('dom')// 循环读取body元素引发线程切换for(let i=0;i<times;i++) {let tmp = document.body}console.timeEnd('dom')// dom: 18.302001953125ms虽然这个例子比较极端,循环次数有些夸张,但如果在循环中包含一些复杂的逻辑或者说涉及到多个元素时,就会造成不可忽视的性能损耗 。
重新渲染
另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)与重绘(Repaint) 。
浏览器在渲染页面时会将 HTML 和 css 分别解析成 DOM 树和 CSSOM 树,然后合并进行排布,再绘制成我们可见的页面 。如果在操作 DOM 时涉及到元素、样式的修改,就会引起渲染引擎重新计算样式生成 CSSOM 树,同时还有可能触发对元素的重新排布(简称“重排”)和重新绘制(简称“重绘”) 。
可能会影响到其他元素排布的操作就会引起重排,继而引发重绘,比如:
修改元素边距、大小
添加、删除元素


推荐阅读