六道经典前端面试题详解

3,770 阅读23分钟

盒模型

盒模型 为页面渲染时,dom元素所采用的布局模型。

盒模型可通过box-sizing进行设置( box-sizing的默认属性是content-box)。根据计算宽高的区域可分为:

  • content-box:W3C 标准盒模型
  • border-box:IE 盒模型
  • padding-box:FireFox 曾经支持
  • margin-box:浏览器未实现

🚨 理论上是有上面 4 种盒子,但现在w3cmdn规范中均只支持content-boxborder-box

盒模型的组成,由里向外content(内容),padding(内边距),border(边框),margin(外边距)

content-box

W3C 标准盒模型的 width 和 height 分別指 content 部分的宽度和高度,在 宽度和高度之外 绘制元素的 padding 和 border

举个例子🌰:

<style>
  #content {
    width: 100px;
    height: 50px;
    border: 5px solid #242424;
    margin: 10px;
    padding: 20px;
    background-color: lightblue;
  }
</style>

<body>
  <div id="content"></div>
</body>

这里我们会发现我们设置了100 * 50的长宽,呈现出来的是一个150 * 100的盒子

这是因为标准盒模型元素宽度(既我们在样式表中设置的width)width = content = 100px

盒子的大小为content + padding + border = 100px + 20px*2 + 5px*2 = 150px

border-box

IE 盒子模型的 width 为 content 部分加上 padding 和 border 的宽度,任何为元素指定的 padding 和 border 都将在已设定好的 宽度和高度之内 绘制

在上面代码的基础上加上box-sizing: border-box:

  <style>
    #content {
      width: 100px;
      height: 50px;
      border: 5px solid #242424;
      margin: 10px;
      padding: 20px;
      background-color: lightblue;
+      box-sizing: border-box;
    }
  </style>

IE 盒子模型元素宽度就是它的盒子大小width = content + padding + border = 100px

其 content 真正的宽度是width - padding - boder = 100px - 20px*2 - 5px*2 = 50px

应用场景 🎉:

border-box更适合在水平布局有多个div,然后各占50%,然后不管你怎么设置border和padding,横向布局都不会因此被挤到下一行

<style>
  .column {
    height: 50px;
    width: 46%;
    margin: 2% 2%;
    float: left;
    background: lightblue
  }
</style>

<body>
  <div>
    <div class="column"></div>
    <div class="column"></div>
  </div>
</body>

如果加上2px的border会被换行:

.column {
  height: 50px;
  width: 46%;
  margin: 2% 2%;
  float: left;
  background: lightblue;
+  border: 2px solid black;
}

加上box-sizing: border-box

.column {
  height: 50px;
  width: 46%;
  margin: 2% 2%;
  float: left;
  background: lightblue;
  border: 2px solid black;
+  box-sizing: border-box;
}

因为设置了为border-box之后,border 和 padding 全都包含在定义的宽高里面。这就意味着一个带有2px边框的div宽度仍然是50%

BFC

概念

Formatting contextW3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。最常见的 Formatting context 有BFCIFC

Box 是CSS 布局的对象和基本单位,元素的类型和display属性,决定了 Box 的类型。不同类型的 Box,会参与不同的Formatting Context

  • block-level box: display的值为block, list-item, table 的元素。并且参与BFC(block fomatting context);
  • inline-level box: display的值为inline, inline-block, inline-table 的元素。并且参与 IFC(inline formatting context)

BFC(Black formatting context) 即块级格式化上下文,它是一块独立的渲染区域,只有Blcok-level box(块级盒) 参与,它规定了内部的Blcok-level box如何布局,并且内部元素的渲染和外界互不影响

  1. 元素满足下面任一条件就会形成BFC:
  • body根元素
  • 浮动元素:float的值不是none
  • 绝对定位元素 position是 absolute | fixed
  • display的值为 inline-block | flex | inline-flex | table-cell | table-caption
  • overflow的值不是visible
  1. BFC的特征(布局规则)
  • 在一个BFC中,行盒(行盒由一行中所有的内联元素所组成,直到当行被占满然后换行)与块盒(块盒会被渲染为完整的一个新行,除非另外指定)都会垂直的沿着其父元素的边框排列。
  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。
  • 每个盒子(块盒与行盒)的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
  • BFC的区域不会与float box重叠。
  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  • 计算BFC的高度时,浮动元素也参与计算。
  1. IFC布局规则

在行内格式化上下文中,框(boxes)一个接一个地水平排列,起点是包含块的顶部。水平方向上的 marginborderpadding在框之间得到保留。框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐。包含那些框的长方形区域,会形成一行,叫做行框。

应用场景

BFC主要应该场景:

  • 阻止margin重叠
  • 可以包含浮动的元素(清除浮动)
  • 可以阻止元素被浮动元素覆盖

阻止margin重叠

<style>
  p {
    width: 100px;
    height: 50px;
    background: lightblue;
    margin: 50px;
  }
</style>

<body>
  <p></p>
  <p></p>
</body>

根据特征2:属于同一个BFC (这里指 body 元素)的两个相邻 Box 的 margin 会发生重叠。所以这里两个 p 标签的间距只剩下 50px;如果想要避免外边距的重叠,可以将其放在不同的 BFC 容器中。

如果想要避免外边距的重叠,可以将任意一个放在 BFC 容器中;

🚨 声明BFC选择器设置overflow为hidden使其成为bfc容器,后续的例子都使用这个选择器

<style>
  p {
    width: 100px;
    height: 50px;
    background: lightblue;
    margin: 50px;
  }
+  .bfc {
+    overflow: hidden;
+  }
</style>

<body>
  <p></p>
+  <div class="bfc">
    <p></p>
+  </div>
</body>

这时候,两个盒子边距就变成了 100px

可以包含浮动的元素(清除浮动)

<style>
  div {
    background: lightpink;
    border: 1px solid lightpink;
    overflow: hidden;
  }
  img {
    float: left;
    width: 100px;
  }
</style>

<body>
  <div>
    <img src="./images/timg.jpg" alt="" />
  </div>
</body>

当我们不给父节点设置高度,子节点设置浮动的时候脱离了文档流,会发生高度塌陷,所以容器只剩下 2px 的边距高度。

根据最后一条特征:计算BFC的高度时,浮动元素也参与计算

要解决高度塌陷的问题,只需要给父元素激活BFC即可,这样父元素就会包裹着浮动元素(清除浮动)

- <div>
+ <div class="bfc">
  <img src="./images/timg.jpg" alt="" />
</div>

可以阻止元素被浮动元素覆盖

<style>
  .right {
    background: lightpink;
    height: 200px;
  }
  img {
    float: left;
    width: 100px;
  }
</style>

<body>
  <div>
    <img src="./images/timg.jpg" alt="" />
    <div class="right">hi girl!</div>
  </div>
</body>

这时候其实第二个元素有部分被浮动元素所覆盖(但是文本信息不会被浮动元素所覆盖) ;

根据特征4:BFC的区域不会与float box重叠。

如果想避免元素被覆盖,可以让right单独成为一个BFC:

<div>
  <img src="./images/timg.jpg" alt="" />
-  <div class="right">hi girl!</div>
+  <div class="right bfc">hi girl!</div>
</div>

层叠上下文

网页及其每个元素都有一个坐标系统,而 HTML 元素沿着其相对于用户的一条虚构的z 轴排开,层叠上下文 就是对这些 HTML 元素的一个三维构想,众 HTML 元素基于其元素属性按照优先级顺序占据这个空间。

层叠等级

层叠等级 (stacking level),也叫 层叠级别层叠水平 是描述元素层叠顺序的一个名词,它决定了元素在 Z 轴 上的显示顺序。层叠等级越高,则展示在越前面

🎉 你可以把创建了层叠上下文的元素理解为该元素当了官,而其他非层叠上下文元素则可以理解为普通群众。当官的等级更高因此在Z轴上更靠近观察者。

这当官的家里都有丫鬟和管家什么的。同样是当官的,把县长家里的管家和巡抚家里的管家作比较实际上是没有意义的,一人得道鸡犬升天,他们牛不牛逼完全由他们主子决定。只有自己内部下属相互比较大小高低才有意义。

类比回“层叠上下文”和”层叠等级“,就得出以下结论:

  1. 创建了层叠上下文的元素比其他元素层级高。
  2. 普通元素的层叠水平优先由其所处的层叠上下文决定
  3. 层叠水平的比较只有在当前层叠上下文元素中才有意义
  4. 在同一个层叠上下文中,它描述定义的是该层叠上下文中的层叠上下文元素在Z轴上的上下顺序。

触发条件

  • 文档根元素(html:<html></html>本身就具有层叠上下文,称为“根层叠上下文”。)
  • position 值为 absolute、relative 的元素,且z-index 值不为 auto
  • position 值为 fixed、sticky 的元素
  • css3属性
    • z-index 值不为 autoflex 容器的子元素(父元素 display : flex | inline-flex )
    • z-index 值不为 autogrid 容器的子元素
    • 元素的 opacity 值不是 1
    • mix-blend-mode 属性值不为 normal 的元素
    • 元素的 transformfilterperspectiveclip-pathmaskmask-imagemask-border 值不是 none
    • isolation 属性值为 isolate 的元素
    • will-change 中指定了任意 CSS 属性,即便你没有直接指定这些属性的值
    • -webkit-overflow-scrolling 属性被设置 touch 的元素
    • contain 属性值为 layoutpaint 或包含它们其中之一的合成值(比如 contain: strictcontain: content)的元素。

层叠顺序

层叠顺序 (stacking Order), 也叫 层叠次序堆叠顺序 描述的是元素在同一个层叠上下文中的顺序规则,由此可见,前面所说的“层叠上下文”和“层叠等级”是一种概念,而这里的“层叠顺序”是一种规则。

在同一个层叠上下文中,层叠顺序从低到高排列(级别高的显示在级别低的前面),如下图所示:

🚨 为什么按照这个层叠顺序呢?诸如borderbackground一般为装饰属性,而浮动和块状元素一般用作布局,而内联元素都是内容。网页中最重要的当然是内容,因此,一定要让内容的层叠顺序相当高,当发生层叠是很好,重要的文字啊图片内容可以优先暴露在屏幕上。

层叠准则

  • 1 首先先看比较元素是否处在同一个层叠上下文中:
    • 1.1 在同一个层叠上下文中,层叠等级才有意义。此时需要比较”内部元素层叠等级“(看“层叠顺序”图),层叠等级大的元素会覆盖层叠等级小的元素,z-index的优先级最高;
    • 1.2 在不同的层叠上下文中,我们比较的是”父级元素层叠等级“。元素显示顺序以”父级层叠上下文“的层叠等级来决定显示的先后顺序,与自身的层叠等级无关;
  • 2 当元素的层叠等级、层叠顺序相同,则在 DOM 流 中处于后面的元素会覆盖前面的元素,遵循”后来者居上“原则;

🚨 需要注意的是判断元素在 Z轴 上的堆叠顺序需要由元素的层叠上下文、层叠等级共同决定。而不仅仅是直接比较元素的 z-index 值的大小.

z-index 属性值并不是在任何元素上都有效果,它仅在 position 属性为非 static 的定位元素上有效果。

示例

🌰 在同一层叠上下文中

<style>
  .box {
    position: relative;
  }
  .a,
  .b,
  .c,
  .d {
    position: absolute;
    width: 100px;
    color: white;
    line-height: 100px;
    text-align: center;
  }
  .a {
    top: 20px;
    left: 20px;
    background: red;
  }
  .b {
    top: 60px;
    left: 60px;
    background: lightblue;
    z-index: 1;
  }
  .c {
    top: 100px;
    left: 100px;
    background: pink;
  }
  .d {
    width: 140px;
    line-height: 140px;
    top: 40px;
    left: 40px;
    background: orange;
    z-index: -1;
  }
</style>

<body>
  <div class='box'>
    <span class="a">a</span>
    <span class="b">b</span>
  </div>
  <div class='box'>
    <span class="c">c</span>
    <span class="d">d</span>
  </div>
</body>

说明: a(红),b(蓝),c(粉),d(橙)的父元素 box 都没有设置z-index,所以不会产生层叠上下文,根据 层叠等级结论2、3 得知此时它们都属于<html></html>标签产生的根层叠上下文中的元素,属于同一个层叠上下文,根据七层层叠顺序:

  • a(红),c(粉) 都没有设置 z-index,同在层叠顺序中的第6级,根据 层叠准则2DOM 流 中处于后面的 c(粉) 会覆盖 a(红);
  • b(蓝) 的 z-index 为正,在第7级;
  • d(蓝) 的 z-index 为负,在第2级;

因此显示顺序为: b(蓝) > c(粉) > a(红) > d(橙)

🌰 在不同一层叠上下文中

<style>
  .box1 {
    position: relative;
    z-index: 1;
  }
  .box2 {
    position: relative;
    z-index: 0;
  }
  .a,
  .b,
  .c {
    position: absolute;
    width: 100px;
    color: white;
    line-height: 100px;
    text-align: center;
  }
  .a {
    top: 20px;
    left: 20px;
    background: red;
    z-index: 10;
  }
  .b {
    top: 60px;
    left: 60px;
    background: lightblue;
    z-index: 20;
  }
  .c {
    top: 100px;
    left: 100px;
    background: pink;
    z-index: 999;
  }
</style>

<body>
  <div class='box1'>
    <span class="a">a</span>
    <span class="b">b</span>
  </div>
  <div class='box2'>
    <span class="c">c</span>
  </div>
</body>

说明: 虽然 c(粉) 的 z-index 为 999,远大于 a(红) 和 b(蓝) 的 z-index,不过他们的父元素创建了新的层叠上下文:

  • a(红),b(蓝) 的父元素 box1 产生的层叠上下文的 z-index 为正,在第7级;
  • c(粉) 的父元素 box2 所产生的层叠上下文的 z-index 为0,在第6级;
  • 根据 层叠准则1.2 box1 产生的层叠上下文的层叠顺序大于 box2 所产生的层叠上下文,所以 box1 中所有元素都排在 box2 上,因此 c(粉) 永远在 a(红),b(蓝) 下面;
  • 根据 层叠等级结论3 由于 a(红),b(蓝)这两个元素都在父元素 box1 产生的层叠上下文中,因此只在内部比较,并且 z-index 为正,同在第7级,所以谁的 z-index 值大,谁在上面;

因此显示顺序为: b(蓝) > a(红) > c(粉)

🌰 CSS3中的属性对层叠上下文的影响

<style>
  .box1 {
    opacity: 0.99;
    position: relative;
  }
  .box2 {
    position: relative;
  }
  .a,
  .b,
  .c,
  .d {
    position: absolute;
    width: 100px;
    color: white;
    line-height: 100px;
    text-align: center;
  }
  .a {
    top: 20px;
    left: 20px;
    background: red;
    z-index: 999;
  }
  .b {
    top: 60px;
    left: 60px;
    background: lightblue;
    z-index: 999;
  }
  .c {
    top: 100px;
    left: 100px;
    background: pink;
  }
  .d {
    width: 140px;
    line-height: 140px;
    top: 40px;
    left: 40px;
    background: orange;
    z-index: -1;
  }
</style>

<body>
  <div class='box1'>
    <span class="a">a</span>
    <span class="b">b</span>
  </div>
  <div class='box2'>
    <span class="c">c</span>
    <span class="d">d</span>
  </div>
</body>

说明: 之前已经介绍了,设置opacity也可以形成层叠上下文,因此:

  • box1 设置了 opacity,box1 成为了一个新的层叠上下文;
  • box2 没有形成新的层叠上下文,根据 层叠等级结论2、3 可以得出其中的元素都属于根层叠上下文;
  • d(橙) z-index 为负属于层叠顺序中第2级
  • a(红),b(蓝) 同在第7级,z-index值相同在 DOM 流 中处于后面的b(蓝)会覆盖 a(红)
  • a(红),b(蓝) 的父元素 box1 和 c(粉) 没设置 z-index 在层叠顺序中第6级,且在 DOM 流 中处于后面的c(粉)会覆盖 box1 ,因此c(粉) 永远在 a(红),b(蓝) 上面;

因此显示顺序为: c(粉) > b(蓝) > a(红) > d(橙)

🚨 如果不设置opacity:

说明: 可以看出 box1,box2都在层叠顺序第6级

  • box1,box2 都没有形成新的层叠上下文,因此其中的元素都属于根层叠上下文,直接拿来比较;
  • a(红),b(蓝) 同属于第7级,z-index值相同在 DOM 流 中处于后面的b(蓝)会覆盖 a(红)
  • c(粉) 没设置 z-index 属于层叠顺序中第6级
  • d(橙) z-index 为负属于层叠顺序中第2级

因此显示顺序为: b(蓝) > a(红)> c(粉) > d(橙)

🌰 和其他元素比较

<style>
  .box1 {
    width: 100px;
    height: 100px;
    background-color: lightblue;
  }
  .box2 {
    margin-top: -50px;
    width: 100px;
    height: 100px;
    background-color: pink;
  }
</style>

<body>
  <div class="box1">box1</div>
  <div class="box2">box2</div>
</body>

说明:

  • box1,box2 都属于根层叠上下文
  • 因为没设置 z-index 所以都在层叠顺序中第6级,在 DOM 流 中处于后面的 box2 会覆盖 box1

因此显示顺序为:box2 > box1

🚨 可以给box1 设置 opacity

  .box1 {
+   opacity: 0.99;
    width: 100px;
    height: 100px;
    background-color: lightblue;
  }

或者给 box1 设置 transform

  .box1 {
+   transform: rotate(15deg);
    width: 100px;
    height: 100px;
    background-color: lightblue;
  }

因为 box1 添加了特定 css3 属性产生了层叠上下文,根据 层叠等级结论1 产生层叠上下文的元素总是比其他元素层高。box1 创建了一个层叠上下文(当官了),所以把 box2 层叠了。

应用场景

<style>
  .box1 {
    float: left;
    width: 100px;
    height: 100px;
    margin-right: -20px;
    background-color: lightblue;
  }
  .box2 {
    overflow: hidden;
    width: 100px;
    height: 100px;
    background-color: pink;
  }
</style>

<body>
  <div class="box1">box1</div>
  <div class="box2">box2</div>
</body>

根据层叠顺序浮动盒子比块盒子层级高,所以 box1 覆盖 box2

当你想要改变元素的层级又不想用定位时,你还可以用很多其他的方法

比如使用了opacity之后块盒子形成层叠上下文,根据 层叠等级结论1 此时块盒子层级比浮动盒子层级高,代码如下:

  .box2 {
    overflow: hidden;
    width: 100px;
    height: 100px;
    background-color: pink;
+    opacity: 0.99;
  }

小结

这里我们可以把层叠上下文想象成一个拥有 z轴 的三维空间,内部层叠上下文元素按照层叠等级排序,并且内部的层叠上下文元素可以还会产生层叠上下文,在比较完父层叠上下文层叠等级后再去比较内部层叠上下文元素的层叠等级,而层叠等级基本由层叠顺序的规则决定。

输入url到页面展示

关于这个经典面试题,可以查看我的另一篇文章,里面介绍了浏览器工作原理的相关知识,理解了这个我们再去理解重绘和回流:

经典面试题!从输入URL到页面展示你还不赶紧学起来?

重绘和回流

在阅读了前面输入url到页面展示一文的 渲染器进程🍊 章节,我相信大家对重绘和回流应该已经有了一些了解,简单总结下:

  1. 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree
  2. 有了RenderTree,我们就知道了所有节点的样式
  3. Layout(回流):根据生成的渲染树,进行回流(Layout),计算得到节点的几何信息(位置,大小)
  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,最后根据计算好的信息把节点绘制到页面上。

回流 (Reflow)

渲染树(Render Tree) 中部分或全部元素的尺寸、结构(位置)、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流也叫重排。见于 渲染器进程🍊 的第 3 步.

这个过程可理解成,将整个网页填白,对内容重新渲染一次。只不过以人眼的感官速度去看浏览器回流是不会有任何变化的,如果足够慢的话就会发现每次回流都会将页面清空,再从左上角第一个像素点从左到右从上到下这样一点一点渲染,直至右下角最后一个像素点。每次回流都会呈现该过程,只是感受不到而已。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变(因为回流是根据视口的大小来计算元素的位置和大小的)
  • 元素尺寸(包括外边距、内边距、边框厚度、宽度、高度等属性改变)或位置发生改变
  • 元素内容变化(input中输入文字、文字数量或图片大小发生变化等等)
  • 元素字体大小变化
  • 添加或者删除可见DOM元素
  • 激活CSS伪类(例如::hover
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()(currentStyle in IE)
  • getBoundingClientRect()
  • scrollTo()

由于每次回流都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化回流过程。

然而,你可能会(经常不知不觉)强制刷新队列并要求计划任务立刻执行。

使用上面列出的这些属性或者调用getComputedStyle、IE 里的 currentStyle方法来获取获取布局信息时,浏览器为了获取最新的布局信息,不得不执行渲染队列中的 “待处理变化” ,并触发回流来即时计算并返回正确的值。

🚨 并且很多⼈不知道的是,重绘和回流其实也和 Eventloop 有关。

  • Eventloop 执⾏完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新⼀次。
  • 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resizescroll 事件也是⾄少 16ms 才会触发⼀次,并且⾃带节流功能。
  • 判断是否触发了 media query
  • 更新动画并且发送事件
  • 判断是否有全屏操作事件
  • 执⾏ requestAnimationFrame 回调
  • 执⾏ IntersectionObserver 回调,该⽅法⽤于判断元素是否可⻅,可以⽤于懒加载上,但是兼容性不好 更新界⾯
  • 以上就是⼀帧中可能会做的事情。如果在⼀帧中有空闲时间,就会去执⾏ requestIdleCallback 回调

重绘 (Repaint)

当一个元素的外观发生改变(例如:color、background-color、visibility、outline等),但没有改变布局(宽高、大小、位置等不变),浏览器会将新样式赋予给元素并重新绘制它,使元素呈现新的外观(这就是为什么重绘不一定会引起回流),这个过程称为重绘。见于 渲染器进程🍊 的第 7 步

为何回流必定引起重绘呢?整个节点的位置都变了,肯定要重新渲染它的外观属性啊!

常见的引起重绘的(改变节点外观的)属性:

  • colorvisibility
  • border-styleborder-radiustext-decorationoutline-coloroutlineoutline-styleoutline-widthbox-shadow
  • backgroundbackground-colorbackground-imagebackground-positionbackground-repeatbackground-size

性能影响对比

回流比重绘的代价要更高。

回流可以说是“牵一发动全身”,有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流,这会带来巨大的计算量

现代浏览器会对频繁的回流或重绘操作进行优化:

浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。 当你访问以下属性或方法时,浏览器会立刻清空队列:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • widthheight
  • getComputedStyle()
  • getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

如何避免

CSS

  • 避免使用table布局,可能一个很小的改动会造成整个table的回流(重新布局)
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。
  • 使⽤visibility替换display: none,因为前者只会引起重绘,后者会引发回流 (改变了布局)
  • 使用transform代替toptop是几何属性,操作top会改变节点位置从而引发回流,使用transform:translate3d(x,0,0)代替top,只会引发图层重绘,还会间接启动GPU加速
  • 使用requestAnimationFrame作为动画帧:动画速度越快,回流次数越多,上述有提到浏览器刷新频率为60Hz,即每16.6ms更新一次,而requestAnimationFrame()正是以16.6ms的速度更新一次。所以可用requestAnimationFrame()代替setInterval()
  • 避免规则层级过多:浏览器的CSS解析器解析css文件时,对CSS规则是从右到左匹配查找,样式层级过多会影响回流重绘效率,建议保持CSS规则在3层左右。
  • 将频繁重绘或者回流的节点设置为图层:图层能够阻⽌该节点的渲染⾏为影响别的节点。⽐如对于 video 标签来说,浏览器会⾃动将该节点变为图层。设置节点为图层的⽅式有很多,我们可以通过will-changevideo 标签iframe标签 来⽣成新图层

🚨 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一

🚨 跟transform一样,opacityfilters这些属性也会使用css3硬件加速(GPU加速),可以让这些动画不会引起回流重绘

css3硬件加速的坑:

  • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

JavaScript

  • 避免频繁操作样式,最好一次性重写style属性,可以使用cssTextclassList或者直接修改 className
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.padding  = '5px'

使用cssText

container.style.cssText = 'width: 100px; height: 200px; padding: 5px;';

使用className

<style>
    .basic_style {
      width: 100px;
      height: 200px;
      padding: 5px;
    }
</style>
<script>
  container.className = 'basic_style';
</script>

使用classList

<script>
  container.classList.add('basic_style')
</script>

前者每次单独操作,都去触发一次渲染树更改,从而导致相应的回流与重绘过程。

合并之后,等于我们将所有的更改一次性发出,用一个 style 请求解决掉了。

  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘,但是会在展示和隐藏节点的时候,产生两次重绘
function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);

如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

优化后:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。 上面代码优化:
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则在动画运动过程中父元素及后续元素会跟随运动导致频繁回流。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
for(let i = 0; i < 1000; i++) {
// 每次循环获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}

应该在循环外使用变量保存一些不会变化的DOM映射值:

const offsetTop = document.querySelector('.test').style.offsetTop;
for(let i = 0; i < 1000; i++) {
    console.log(offsetTop);
}

小结

  • 会引起元素位置变化的就会回流,比如窗口大小、字体大小或者元素位置改变,都会引起周围的元素改变他们以前的位置;而重绘不会引起位置变化的,只是在以前的位置进行改变背景颜色等;
  • 回流必将引起重绘,重绘不一定会引起回流。
  • 回流比重绘的代价要更高。

防抖节流

防抖节流的作用就是防止方法被频繁调用

<div>没有防抖的input: <input id="unDebounce" /></div>
<script>
//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content)
}

let inputa = document.getElementById('unDebounce')

inputa.addEventListener('keyup', function (e) {
    ajax(e.target.value)
})
</script>

可以看到,我们只要按下键盘,就会触发这次ajax请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。

函数防抖(debounce)

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

思路 ✨:

在执行setTimeout函数之前先将timer用clearTimeout清除,取消延迟代码块,确保只执行一次

上面的例子加上防抖后,我们看看效果:

<div>防抖后的input: <input id="debounce" /></div>
<script>
//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content)
}

function debounce(fun, delay) {
    return function (args) {
        let that = this
        let _args = args
        clearTimeout(fun.id)
        fun.id = setTimeout(function () {
            fun.call(that, _args)
        }, delay)
    }
}
    
let inputb = document.getElementById('debounce')

let debounceAjax = debounce(ajax, 500)

inputb.addEventListener('keyup', function (e) {
        debounceAjax(e.target.value)
    })
</script>    

可以看到,我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。

函数节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

思路 ✨:

主要思路利用时间戳判断,每次调用判断和上一次调用的时间差异确定是否需要调用。

<div>节流后的input: <input id="throttle" /></div>
<script>
  function throttle(fun, delay) {
        let last, deferTimer
        return function (args) {
            let that = this
            let _args = arguments
            let now = +new Date()
            if (last && now < last + delay) {
                clearTimeout(deferTimer)
                deferTimer = setTimeout(function () {
                    last = now
                    fun.apply(that, _args)
                }, delay)
            }else {
                last = now
                fun.apply(that,_args)
            }
        }
    }

    let throttleAjax = throttle(ajax, 1000)

    let inputc = document.getElementById('throttle')
    inputc.addEventListener('keyup', function(e) {
        throttleAjax(e.target.value)
    })
</script>  

可以看到,我们在不断输入时,ajax会按照我们设定的时间,每1s执行一次。

简单理解

  • 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。
  • 函数防抖就是英雄回城时,(如果还没回城)我们再次点击回城按钮,会重新计算回城时间

应用场景

防抖(debounce)

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。(用户一直输入会有新触发,停止的时候才会延迟触发)
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
  • 频繁操作点赞和取消点赞,因此需要获取最后一次操作结果并发送给服务器

节流(throttle)

  • 鼠标不断点击触发,mousedown(单位时间内固定只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

最后

如果总结有误,欢迎纠正讨论!

参考文章 📜

❤️ 7分钟理解JS的节流、防抖及使用场景

❤️ 10 分钟理解 BFC 原理

❤️ 彻底搞懂CSS层叠上下文、层叠等级、层叠顺序、z-index

❤️ CSS 中重要的层叠概念

❤️ 浏览器的回流与重绘 (Reflow & Repaint)

❤️ 你真的了解回流和重绘吗

❤️ 玩转CSS的艺术之美