0%

DOM & BOM

本章主要介绍DOM概念、操作DOM的代价、浏览器渲染引擎的机制,性能优化以及节流和防抖等常见面试题。

DOM

1. 什么是DOM

DOM就是Document Object Model文档对象模型,里边是接口,即方法函数。

官方定义:DOM是一个**独立于语言的、用于操作XML和HTML文档的程序接口(API)**。在浏览器中主要用于与HTML文档打交道,并且使用DOM API用来访问文档中的数据。
DOM是个与ES语言无关的API,它在浏览器中的接口却是用JavaScript来实现的,DOM就成了现在JS编码中的重要部分。

2. JS修改DOM元素代价很高

每次操作DOM之前,就会先访问DOM,所以也会消耗性能。
在此基础上,因为修改DOM会导致浏览器重新计算页面的几何变化、引发浏览器模板引擎的重排(回流 - 回滚流程)和重绘,进而更加消耗性能。

3. 浏览器渲染引擎的工作原理和流程

浏览器下载完页面中的所有资源(比如HTML、JavaScript、CSS、图片等)后,会发生如下的6步过程:

  1. 解析HTML,构建DOM树(DOM Tree)

  2. 解析CSS,生成CSS规则树(CSSOM Tree)

  3. 合并DOM树和CSS规则树,生成渲染树render树(render Tree)

  4. 布局render树,根据生成的render树来对各元素尺寸、位置进行计算,得到每个节点的几何信息。(根据视口的大小来计算元素的位置和大小)(重排会走这一步)

  5. 绘制render树,绘制页面像素信息(根据render树上每个节点的几何信息,得到每个节点的像素数)(重绘会走这一步)

  6. 浏览器会将各层节点的像素信息发送给GPU,GPU将各层合成、绘制展示到页面上

3.1 浏览器渲染引擎是如何生成渲染树(render Tree)的?

  1. 从DOM Tree的根节点开始遍历每一个可见节点(除meta、link、script等这些标签;除display:none;的元素)
  2. 对于每个可见节点,在CSSOM中找到对应规则并将样式规则应用到对应节点上。
  3. 根据每一个可见节点,以及其对应的样式,组合生成渲染树。

不可见节点: 不会渲染输出的节点(不会显示在屏幕上的节点)有以下几种

  • meta、link、script等标签;
  • 通过css进行隐藏的节点,即display:none;(opacity对人类不可见,计算机还能看见,所以还会渲染。)(那visibility为隐藏的元素会不会被渲染呢?做个试验,一个div设置visibility不可见,左浮动,周围全是文字,看文字环绕是否让出一块空白区域。最后试验证明确实绕出了一段空白的位置,说明visibility和opacity设置的不可见只是对人类肉眼不可见,计算机还是会在生成render Tree的时候计算位置信息并把他绘制出来。试验结果如下图:)

4. 什么是浏览器渲染引擎的重排和重绘?

4.1 重排

DOM的变化影响了元素的几何属性(宽、高和位置),浏览器需要重新计算元素的几何属性,同样其他相邻元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为“重排”。

4.2 重绘

完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘
因为重排在重绘的上一步,所以重排发生后自然会导致重绘

5. 什么时候会引发重排?

当页面布局和几何属性改变时就需要重排:
(核心就是:只要某个属性能导致位置信息发生改变,就会触发重排 )

  1. 添加或删除可见的DOM元素。(一堆人排队,添加即中间插入了一个人/删除即中间一个人走了,势必会影响后边排队的人的位置信息也发生改变)

  2. 元素位置改变(重排就是因为位置信息改变了)

  3. 元素尺寸改变( 外边距、内边距、边框厚度、宽度、高度等)

  4. 内容改变,例:文本数量/内容改变、或图片被另一个不同尺寸的图片替代、字体大小改变、(文字加粗?)导致DOM元素位置、面积改变。【计算会消耗CPU的能力】

  5. 页面渲染器初始化(这算重走流程吧,肯定要重排)

  6. 浏览器窗口尺寸改变(位置信息会被迫调整,发生重排。见下图的gif图,一个页面中div元素的位置不受视口调整而修改,也会引发重排)【消耗GPU的计算能力】。试验:resize视口,一个页面中div元素的位置不受视口调整而修改,也会引发重排

6. 什么时候会引发重绘?

  1. 重排必然引发重绘,这是肯定的。因为浏览器的工作流程就是排版后渲染。重排会回流(回滚流程)到排版阶段,排版后需要重新绘制页面。

  2. 单独触发重绘的情况:
    除元素尺寸、位置发生改变以外的情况,(比如字体颜色、背景色等发生改变)。(我怀疑文字加粗也会触发重排,但是我没有证据。理论上来说如果在一个固定尺寸的div内加粗文字,应该不会影响后边元素的重排,但可能该div内部的其他相邻文字或元素会发生重排。)

7. 浏览器的性能优化

7.1 优化

现代浏览器是相当完善的了,因为多次操作DOM会触发重排重绘、消耗性能。所以除了我们人为的、有意识的去控制操作DOM次数以外,浏览器在设计上进行了优化,也会智能的“节流”操作DOM,比如实现队列化修改、批量执行。

解释来说就是,浏览器会有一个“队列”,用以存放(攒着)需要操作DOM的js程序。每当执行一次js操作dom的代码,这个队列里就先暂存一个程序。等到一段时间后,浏览器再集中、批量的链接一次”ES岛”和”DOM岛”(就是让JS引擎去链接渲染引擎),进而触发一次DOM操作。你可以形象的理解为“过一段时间发一班车”。

7.2 用户打断优化的操作

但是我们人类感知不到啊,可能会因为误操作打断浏览器的“节流”步骤。迫使浏览器中断当前的“等待”,去赶紧、立马进行一次dom操作。让浏览器赶紧执行完他攒在“队列”里的JS操作DOM的程序后返回最新的DOM位置信息给我们。这就好像电梯门定时自动关闭,但是你却手动按了关门按钮强迫关门一样。

这种情况就发生在我们获取DOM信息的时候:

打断浏览器优化,强迫触发重排的属性:

offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()

因为要跟浏览器请求最新的DOM信息,所以浏览器就得赶紧让JS引擎去渲染引擎那里进行一次DOM操作。

8. 为什么操作DOM非常昂贵?

  1. ES和 DOM是两种东西,每次连接都需要消耗性能
  2. 操作DOM会导致重排和重绘,重排会占用、消耗CPU; 重绘会占用、消耗GPU

9. 防抖 & 节流 — 源自思否-安歌

9.1 解释

案例:大多网站会提供这么一个按钮:用于返回顶部。

这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求– 监听浏览器滚动事件,返回当前滚条与顶部的距离。可以用一个函数实现。

但是,在运行的时候会发现存在一个问题:这个函数的默认执行频率,太!高!了!。 高到什么程度呢?以chrome为例,我们可以点击选中一个页面的滚动条,然后点击一次键盘的【向下方向键】,会发现函数执行了8-9次

然而实际上我们并不需要如此高频的反馈,毕竟浏览器的性能是有限的,不应该浪费在这里,所以接着讨论如何优化这种场景。

9.2 防抖(debounce)

简言之,防抖是如果在短时间内大量触发相同事件,不立即执行,而是等待一个给定时间段后再执行,如果给定时间内又触法该事件则重新计时

基于上述场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:

  • 如果在200ms内没有再次触发滚动事件,那么就执行函数
  • 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数。

9.3 节流(throttle)

简言之,节流是如果在短时间内大量触发相同事件,即每隔相同一段时间执行一次

继续思考,使用上面的防抖方案来处理问题的结果是:

  • 如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。

新需求:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?

其实很简单:我们可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间)。

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。