BEM规范

BEM 规范是什么

BEM 是 块(block) 、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种 css 类命名约定。

  • 连字符:表示某个块或块子元素的多单词之间的连接符号

__ 双下划线:连接块和块子元素

– 双连字符:作为块或块子元素的修饰符号

以这段 html 为例:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="article">
<div class="article__body">
<div class="article__body-left">
<div class="article__body-logo">
<img class="icon" />
</div>
</div>
<button class="button button--primary"></button>
<button class="button button--default"></button>
</div>
<div class="article__footer article__footer--primary"></div>
</div>

block

block 表示组件的顶级抽象,它仅仅作为一个边界,不应该在 block 上添加样式和修饰

1
2
3
4
5
6
7
8
9
10
11
// 正确
.article {
&__body {
background: red;
}
}

// 错误
.article {
background: red;
}

element

element 表示 block 下的子元素,该元素依赖于块。

1
2
3
4
5
6
7
.article {
&__body {
}

&__footer {
}
}

如果元素下还嵌套子元素,均可用 **-** 连接,这样可以清楚的知道该元素的父级。

**.icon****.button** 这种可以独立存在的子元素,避免创建不必要的父级。
假如把 .button 命名为 .article__body-button ,后续的开发人员要在** **.article__footer 里用 .article__body-button 的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="article">
<div class="article__body">
<div class="article__body-left">
<div class="article__body-logo">
<img class="icon" />
</div>
</div>
<button class="article__body-button button--primary"></button>
<button class="article__body-button button--default"></button>
</div>
<div class="article__footer article__footer--primary">
<!-- 假设这里要用到article__body-button的样式 -->
<button class="article__body-button"></button>
</div>
</div>

可以看到,.article__body-button.article__footer 下,这会使代码变得混乱和不一致,应该避免这种情况的出现。

modifier

修饰符,改变块或元素的样式。
.button--primary.article__footer--primary

BEM 规范的优点

  1. 当想要创建新组件时,我们可以容易的知道哪些修饰符和子组件已存在
  2. 从 html 结构上,能快速知道元素的依赖关系,如,我们用连接符 - 表示元素下的嵌套
  3. 统一的命名方式,方便团队成员的阅读

总结

推荐用法:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="article">
<div class="article__body">
<div class="article__body-left">
<div class="article__body-logo">
<img class="icon" />
</div>
</div>
<button class="button button--primary"></button>
<button class="button button--default"></button>
</div>
<div class="article__footer article__footer--primary"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.article {
&__body {
&-left {
}
&-logo {
}
.icon {
}
.button {
&--primary {
}
&--default {
}
}
}
&__footer {
&--primary {
}
}
}
  • block 不要添加修饰符和样式
  • element 下,子元素均用连接符 - 连接
  • element 下,没有后代的元素可用单个单词表示,方便复用

html和css的布局规则

html/css 的布局规则:

想了解 html/css 的布局规则,重要的不是规则是什么,而是什么因素会影响你使用对应的规则。当你理解了这些因素,那么相同场景下,你自然而然会想到使用什么规则。

场景(场景和关联)

举个例子:

  1. 土地:大小,价格,建什么东西

一句话概括就是:往容器放东西,怎么放。
土地:容器
大小和价格: 因素
东西:元素
怎么放:规则

那么,应用到浏览器中:
容器:也就是我们的浏览器视口(viewport)
因素:矩形、坐标系、边界
元素:盒模型
规则:文档流、定位、浮动、BFC、flex 等等

前面提到,要了解布局规则,重要的不是规则是什么,而是什么因素会影响你使用对应的规则,那么我们分别介绍下容器、因素、元素、规则,并从中建立关联。

容器(viewport):

web 浏览器视口指的是我们可见的区域,不包括浏览器菜单。移动设备的视口默认值为 980px,一般情况下这比移动设备要大,那么内容就会显示不完全,如果直接缩放那会导致字体变小。为了让视口的值等于移动设备的宽度,我们会在 html 的头部添加以下标签:

1
<meta name="viewport" content="width=device-width" />

因素:

  1. 矩形:可缩放 -> 响应式布局
  2. 坐标系:拖拽、视差 -> 定位、滚动条、层叠上下文元素
  3. 边界:视口有边界、坐标系无边界

可见,容器的不同因素直接影响着我们在实现某种效果时使用哪些规则。接下来看看元素。

元素:

  • 标准盒模型:块模型、行内模型
  • 怪异盒模型
  • 弹性盒模型

先了解下盒模型的结构,从里到外:content → padding → border → margin,各个部分组合在一起就是我们页面上看到的内容
pic.1708406175712

标准盒模型:

我们常说的盒模型指的是 W3C 标准盒模型

  • 元素的widthheight只包含 content 区域
  • 盒子实际大小等于width+padding+border

盒子的实际宽度我们可以看做是总宽度,和元素本身设置的 width 不是一个概念

pic.1708406185130

我们布局中广泛应用的模型,分为块模型和行内模型,我们叫做块元素和行内元素。
块元素
display: block可以将元素设置为块元素,它拥有以下行为:

  • 它占据父元素的所有宽度,绝大数情况下(不包含行内块元素)
  • 独占一行
  • 拥有widthheight属性
  • 它的marginborderpadding 属性会将盒子周边的元素给“推开”

行内元素
display: inline可以将元素设置为行内元素,也叫行内元素,它拥有以下行为:

  • 盒子不会独占一行
  • widthheight 属性不起作用
  • 水平方向的外边距、内边距、边框会被应用,且会把其它 inline 状态的盒子推开
  • 垂直方向的外边距、内边距、边框会被应用,但不会把其它 inline 状态的盒子推开

默认为行内元素有:a、span、em、strong

除了标准盒模型,还有怪异盒模型,也可以称 IE 盒模型。因为 IE8 之前,IE 默认使用怪异盒模型,并且没有可用的机制来切换。

怪异盒模型

  • 元素的widthheight不仅包括content,还包括paddingborder
  • 盒子实际的大小取决于 width

pic.1708406192021

图中我们给盒子设置了width: 200pxpadding: 10px,那么 content 就会被挤压为 180px(200 - padding * 2)。

应用:使用怪异盒模型能解决 1px 边框线问题。

box-sizing

box-sizing 属性可以切换盒模型模式,默认值是content-box,可选择有border-boxinherit
content-box:w3c 标准盒模型
border-box:IE 盒模型
inherit:从父元素集成 box-sizing 属性的值

弹性盒模型

对于一些特殊布局,如水平垂直居中,采用传统布局不容易实现,采用弹性盒模型就很方便实现。

推荐阮一峰老师的文章:Flex 布局教程,再补充容易忽视的几点:

作用在 flex 容器上
display: flex:
默认: align-items: stretch、flex-direction: row
align-content:
指定如何在纵轴上 项目之间和周围 分配空间。单轴下(flex-wrap: nowrap)此属性不生效
align-items:
初始值:stretch
stretch:表示 flex 项目会被拉伸至最高的项目高度的高度
flex-flow: row wrap === flex-direction: row + flex-wrap: wrap

作用在 flex 项目上
flex-grow:
初始值:0
flex-shrink:
初始值:1
flex-basic:
初始值:auto,即项目本身的大小
指定了 flex 项目在主轴上的初始大小(如果主轴是水平轴,那就是 flex 项目的宽,否则是高)
flex: 1 200px === flex: 1 + flex-basic: 200px
指定了 flex 项目在主轴上初始化(width: 200px)后,按比例分配剩余空间

flex 缩写
flex === flex-grow flex-shrink flex-basic

flex: 1
flex: 1,此属性可以按同等比例分配项目的大小,那么它的完整写法是什么呢?

不等于 1 1 auto
重点理解下缩写下 flex-basic 的作用:
在按比例分配项目之前,计算项目是否有多余的空间,默认为 auto,即项目自身的大小。
如果设置为 auto,那么按照项目自身大小初始化后等比例分配剩余空间,也就会出现项目大小不一样的情况,所以不是 1 1 auto。

等于 1 1 带单位的长度值
假设我们设置为 1 1 0px,按照 0px 初始化后等比例分配剩余空间,那每个项目的大小就会相同。

flex 常用布局

需求:同一行内,如果有一个标题占两行,整行标题都占两行,否则默认一行。标题超出两行时用省略号表示,效果如下:
pic.1708406202621
这个需求的难点在于卡片的高度不是固定的。有什么办法可以让同行内的标题高度保持统一?

先看一个两行超出隐藏的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
.title {
width: 200px;
background-color: yellow;

word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.title2 {
background-color: pink;
}
1
2
3
4
<div class="title">
翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁
</div>
<div class="title title2">请请请请请请请请请</div>

pic.1708406216076
先来重点理解display: flex和 flex 下的height: 100%

  • 当我们给容器设置 display: flex 时,那么 align-items 属性的值为 stretch,即同一行内的 flex 项目会被拉伸至最高的项目高度的高度
  • 给 flex 项目设置 height: 100%,会填充容器剩余高度。

实现:

  1. 把容器设置为弹性盒子display: flex
  2. 给标题设置height: 100%
  3. 给标题兄弟元素设置flex-shrink: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="section">
<div class="card">
<div class="card-content">1</div>
<div class="card-title">
请问请问群翁群翁群翁群问请问群翁群翁群翁群问请问群翁群翁群翁群
</div>
</div>
<div class="card">
<div class="card-content">2</div>
<div class="card-title">2222222222</div>
</div>
<div class="card">
<div class="card-content">3</div>
<div class="card-title">3333</div>
</div>
<div class="card">
<div class="card-content">4</div>
<div class="card-title">444</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.section {
display: flex; // 同一行内的flex项目会被拉伸至最高的项目高度的高度
flex-wrap: wrap;
width: 300px;
}
.section .card {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid red;
margin: 0 10px;
}
.section .card .card-content {
flex-shrink: 0; // 防止被标题height: 100%属性挤压

width: 100px;
height: 100px;
background-color: pink;
}
.section .card .card-title {
height: 100%; // 填充容器剩余高度

line-height: 20px; // 加上行高样式更稳定
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
background-color: yellow;
}

规则

下面介绍文档流、定位、浮动、BFC 等布局规则。

文档流:

在不对页面元素进行任何控制的情况下,浏览器默认的布局方式。从上到下,从左到右。

定位(position):

解决某个元素随页面发生变化而固定位置,它不作为一种主要布局方式,而是用于管理和微调页面中一些特殊项的位置

  • 静态定位:默认值,不做任何
  • 相对定位:相对自己定位,不会脱离文档流
  • 绝对定位:相对于 html,会脱离文档流,或相对于最近被定位的祖先元素定位。多配个相对定位使用
  • 固定定位:相对于浏览器视口固定定位
  • 粘性定位:初始像静态定位,当它的位置相对视口达到预设值时,就会想固定定位一样被固定住

浮动(float)

在传统布局里,信息是纵向排列(从上到下)的,浮动可使信息横向排列。浮动会脱离文档流,正常文档流的元素会围绕着浮动元素。

  • left:左浮
  • right:右浮
  • none:不浮动
  • inherit:继承父元素浮动属性

在使用浮动时,我们得清楚的知道我们要把元素摆放在哪个位置。因为使用浮动时,可能会存在可视化布局与源顺序不同。

假设现源顺序布局为如下

1
div1 div2 div3

我们给 div2 和 div3 同时设置 float: right,那么视觉效果就为:

1
div1 div3 div2

这是因为 div2 在源顺序上比 div3 等级更高(在 DOM 上,div2 先出现,并声明了 float: right),所以在视觉上更靠右。

BFC:块级格式化上下文(block format context )

定义:可以看作是一块独立的区域空间,拥有普通盒子没有的一些特性。它解决个体之间的位置交互问题。

触发 BFC 的方法:

  • html 根元素
  • position: absolute、fixed
  • display: flex、inline-block、table-cell
  • overflow: 除 visibility
  • float: 除 none

来看下元素间的交互问题以及解决方法:

  • 同一块 BFC 下,相邻垂直方向盒子外边距会发生重叠:
1
2
3
4
<div class="box">
<div class="child"></div>
<div class="child"></div>
</div>
1
2
3
4
5
6
7
8
9
10
.box {
width: 150px;
border: 1px solid red;
}
.child {
height: 50px;
width: 50px;
background-color: blueviolet;
margin: 50px;
}

pic.1708406228923
我们给每个 child 设置了margin: 50px,但它们的间距不是 100px,而是 50px,如果我们想让间距变成 100px,那么只要给其中一个盒子设置为 BFC,这样就不再同一块 BFC 下

1
2
3
4
5
6
<div class="box">
<div class="wrap">
<div class="child"></div>
</div>
<div class="child"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
width: 150px;
border: 1px solid red;
}
.child {
height: 50px;
width: 50px;
background-color: blueviolet;
margin: 50px;
}
.wrap {
overflow: hidden;
}

pic.1708406235633

  • 浮动问题:
1
2
3
<div style="border: 1px solid #000;">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>

pic.1708406243403
为外层 div 添加overflow: hidden

1
2
3
<div style="border: 1px solid #000;overflow: hidden;">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>

pic.1708406250328

  • 元素被浮动元素覆盖的问题
1
2
3
4
5
6
7
8
9
<div style="height: 400px;">
<div style="height: 100px;width: 100px;float: left;background: lightblue">
我是一个左浮动的元素
</div>
<div style="width: 200px; height: 200px;background: #eee;">
我是一个没有设置浮动, 也没有触发 BFC 元素, width: 200px; height:200px;
background: #eee;
</div>
</div>

pic.1708406257409
给被浮动元素覆盖的元素添加overflow: hidden

1
2
3
4
5
6
7
8
9
<div style="height: 400px;">
<div style="height: 100px;width: 100px;float: left;background: lightblue">
我是一个左浮动的元素
</div>
<div style="width: 200px; height: 200px;background: #eee;overflow: hidden;">
我是一个没有设置浮动, 也没有触发 BFC 元素, width: 200px; height:200px;
background: #eee;
</div>
</div>

pic.1708406277606
小结:
解决塌陷:给其中一个盒子设置为 BFC,这样就不再同一块 BFC 下
清除浮动:给浮动元素父元素设置为 BFC
防止元素被浮动元素覆盖:给元素设置 BFC,就不会被浮动元素覆盖

IFC:行内格式化上下文(inline formatting context)

pic.1708406355114
让块水平居中

1
2
3
4
5
6
7
8
9
10
11
12
.p {
width: 100px;
background-color: pink;

text-align: center;
}
.child {
width: 50px;
background-color: yellow;

display: inline-block;
}
1
2
3
<div class="p">
<div class="child">12</div>
</div>

pic.1708406370175

总结(对 html/css 规则)

抽象点来说,容器就是一个可以容纳个体的空间,当我们把一个个个体放到容器时,容器的因素决定了我们使用什么规则来存放从而更加合理。
具体点来说,视口就是我们的容器,元素就是个体,视口的因素(矩形、坐标系和边界等)决定了我们使用什么规则来布局。元素分为标准盒模型、IE 盒模型和弹性盒模型,规则分为文档流、定位、浮动、BFC 等等。

参考:
https://zhuanlan.zhihu.com/p/183050328
https://blog.csdn.net/weixin_41682025/article/details/110343448

z轴的学习规则

html 元素沿着相对其用户的一条虚构的 z 轴排开,层叠上下文就是对这些 html 元素的三维构想。
pic.1708406510231

背景

在 z 轴上,存在层叠上下文,他们有着遮挡关系的先后顺序,我们可以称之为层叠顺序,学习层叠上下文主要就是学习层叠顺序。在学习前,我们先来了解背景,我们试想一下,页面上为什么需要有 z 轴的存在?

举个例子:
常见的“顶部固定不动,内容区域向上滚动”的效果,我们给顶部设置position: fixed,内容区域设置overflow: auto。假设我们的内容在第一层,那顶部就在第一层之上,它不随第一层的滚动而滚动,因为 z 轴上的每一层都是相互独立的,这样轻而易举的达到了我们想要的效果。

如果不存在 z 轴,他们处在同一层,有可能实现这种效果吗?答案是有。内容向上滚动的同时,滚了多少距离,我们的顶部就向下滚动多少距离,这也可以实现相同的效果。但是这个过程的计算过程巨复杂,它使不变的元素(顶部)也进行了重新的计算,那么 GUI 渲染线程会重新计算 render tree,然后重新绘制页面元素。这将导致我们的页面性能非常差。

再举个常见的例子,比如视差、canvas,也是同个道理,都是为了更少的计算,让不变的保持不变。而这背后的原理,就是为了迎合浏览器的规则,减少回流。

所以答案就是:z 轴的存在,其实是网页性能优化的一种手段。使不变的元素保持不变,减少回流。

层叠上下文

我们可以把它们看做成一个个独立的空间,它们在 z 轴上存在顺序上的层叠关系。
层叠上下文存在以下特性

  • 层叠上下文可以包含在其他层叠上下文当中,形成一个嵌套层级
  • 每个层叠上下文完全独立于其他兄弟元素

层叠等级

层叠等级,也可以叫层叠级别/层叠水平,在同一个层级上下文中,等级越高,越靠近用户。

  • 普通元素的层叠等级由所处层叠上下文决定
  • 在同一个层叠上下文中,等级越高,越靠近用户
  • 不在同一个上下文中的层叠等级比较是没有意义的

用代码验证一下第三点:

1
2
3
4
5
<div class="yellow">
1
<div class="blue">999</div>
</div>
<div class="red">2</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.yellow {
background: yellow;
height: 100px;
position: relative;
z-index: 1;
}
.blue {
background: blue;
height: 100px;
position: absolute;
z-index: 999;
}
.red {
background: red;
width: 50px;
height: 50px;
position: relative;
left: 20px;
top: -20px;
z-index: 2;
}

pic.1708406521813

.yellow的层叠等级为 1,.red的层叠等级为 2,.blue为 999,.yellow.red在同一个根层叠上下文下。
但是由图可见,.blue.red下面,这是因为.yellow的层叠等级比.red小,也证明了不在同一个上下文中的比较是没有意义的。

创建层叠上下文

  • html为根层叠上下文
  • position属性为absoluterelative并设置了z-index属性为具体数值
  • position值为fixedsticky的元素
  • css3 的一些属性(文章后面介绍)

层叠顺序

层叠规则,表示元素在发生层叠时,有着特定的层叠顺序。层叠上下文和层叠等级是概念,而层叠顺序是规则。
层叠上下文的 background/border < z-index 负值 < block 元素 < float 元素 < inline-block/inline < z-index: 0 / z-index: auto < z-index: 正值
层叠顺序图.png

DOM 顺序和层叠顺序

从图中可以看出,层叠低的在层级高的下面,就像往容器里面放东西一样。它其实迎合栈的层叠规则,先进后出。所以浏览器会优先渲染层叠等级低的元素,再渲染比它高的。

所以结合实际的应用,在编码时,我们应该自然而然的把层叠等级低的写在最前面,让渲染更流畅。

1
2
3
<div style="z-index: -100">div1</div>
<div style="z-index: 0">div2</div>
<div style="z-index: 1">div3</div>

如果层级高的在下面会怎样?看个例子
我们先往容器(html)里放了一个 div1,在放 div2,div3,假设 3 个 div 都是层叠元素,div1 层叠等级最高,并且在最下面

1
2
3
<div style="z-index: 1">div1</div>
<div style="z-index: 0">div2</div>
<div style="z-index: -100">div3</div>

以上代码,假设先渲染等级高的(div1),那渲染等级低的元素时,是不是还要把它上面的元素先拿出来。

万能公式

  • 同一个层叠上下文,谁的层叠等级大,谁再上面。
  • 不在同一个层叠上下文,找到它们的父级为兄弟节点,再比较。
  • 当层叠等级和层叠顺序相同时,后来居上。

下面我们来验证一下层叠顺序和万能公式是否准确。
例子 1:z-index: 负值 < block 元素:

1
2
3
4
5
6
<div class="box">
<div class="parent">
parent
<div class="child">child</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.box {
}
.parent {
width: 200px;
height: 100px;
background: #168bf5;
}
.child {
width: 100px;
height: 200px;
background: #32d19c;
position: relative;
z-index: -1;
}

pic.1708406544047

.parent 是个普通的块元素,.child 是带负值的 z-index 层叠上下文元素,.parent 在.child 上面,所以 z-index: 负值 < block 元素成立。

例子 2: 层叠上下文的 background/border < z-index 负值
继续上个例子,我们给.parent 添加 position: relative 和 z-index: 0,其余代码不变

1
2
3
4
5
6
<div class="box">
<div class="parent">
parent
<div class="child">child</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.box {
}
.parent {
width: 200px;
height: 100px;
background: #168bf5;
position: relative;
z-index: 0;
}
.child {
width: 100px;
height: 200px;
background: #32d19c;
position: relative;
z-index: -1;
}

pic.1708406555021

这时候.parent 变成了带 background 的层叠上下文元素,.child 依然是带负值的 z-index 层叠上下文元素。网上的很多文章拿这个例子来验证 background/border < z-index 负值,其实是不正确的。.child 在.parent 上,是因为.child 嵌套在.parent 里面,所以.child 在上面。也就是说他们不在用一个层叠上下文之下,所以他们的比较是没有意义的。

关于 background/border < z-index 负值,我还没想到如何验证。

例子 3:block 元素 < float 元素 < inline-block/inline

1
2
3
4
<div class="box">
<img src="../../images/js1.jpg" alt="" />
<span>我是重要的内容,我的等级很高的哦</span>
</div>
1
2
3
4
5
6
7
8
9
10
11
.box {
background: red;
height: 300px;
}
img {
float: left;
}
span {
position: relative;
left: -30px;
}

pic.1708406566018

.box 是块元素,img 是浮动元素,span 是 inline 元素,inline 在最上面,img 在中间,所以block 元素 < float 元素 < inline-block/inline成立。

例子 4:同一个层叠上下文,谁的层叠等级大

1
2
3
4
5
6
7
8
9
10
<div class="box">
<div class="box1">
<!-- 盲僧 -->
<img src="../../images/ms.jpg" alt="" />
</div>
<div class="box2">
<!-- 剑圣 -->
<img src="../../images/js1.jpg" alt="" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.box1 {
position: relative;
z-index: auto;
}
.box1 img {
position: absolute;
z-index: 2;
}
.box2 {
position: relative;
z-index: auto;
}
.box2 img {
position: relative;
z-index: 1;
}

pic.1708406574167

.box1 和 .box2 的 z-index 值为 auto,此时不是层叠上下文元素,两张图片都属于在根层叠上下文下,因为盲僧在剑圣上面,所以同一个层叠上下文,谁的层叠等级大成立

例子 5:层叠等级和层叠顺序相同时,后来居上
在例子 4 的基础上,我们把.box1 和.box2 的 z-index: auto 改为 z-index: 0

1
2
3
4
5
6
7
8
9
10
<div class="box">
<div class="box1">
<!-- 盲僧 -->
<img src="../../images/ms.jpg" alt="" />
</div>
<div class="box2">
<!-- 剑圣 -->
<img src="../../images/js1.jpg" alt="" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.box1 {
position: relative;
z-index: 0;
}
.box1 img {
position: absolute;
z-index: 2;
}
.box2 {
position: relative;
z-index: 0;
}
.box2 img {
position: relative;
z-index: 1;
}

pic.1708406589649

此时.box1 和.box2 都变成了层叠等级为 0 的层叠上下文元素,并且在同个层叠上下文下。剑圣在盲僧上面,所以层叠等级和层叠顺序相同时,后来居上成立。

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

介绍几个比较常用的,如下:

  1. 父元素的 display 属性值为 flex | inline-flex,子元素的 z-index 属性值不为 auto,子元素为层叠上下文元素
  2. 元素的 opacity 值不为 1
  3. 元素的 filter 属性值不是 none
  4. 元素的 transform 属性值不是 none

例子 6:父元素 display: flex | inline-flex 与层叠上下文

1
2
3
4
5
6
<div class="box">
<div class="parent">
parent
<div class="child">child</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.box {
}
.parent {
width: 200px;
height: 100px;
background: #168bf5;
/* 虽然设置了z-index,但是没有设置position,z-index无效,.parent还是普通元素,没有产生层叠上下文 */
z-index: 1;
}
.child {
width: 100px;
height: 200px;
background: #32d19c;
position: relative;
z-index: -1;
}

pic.1708406603779
继续例子 6,我们把.parent 变成层叠上下文元素,
例子 7:层叠上下文的 background/border < z-index 负值

1
2
3
4
5
6
<div class="box">
<div class="parent">
parent
<div class="child">child</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.box {
display: flex;
}
.parent {
width: 200px;
height: 100px;
background: #168bf5;
/* .box设置了display: flex; 这时候.parent变成了层叠上下文 */
z-index: 1;
}
.child {
width: 100px;
height: 200px;
background: #32d19c;
position: relative;
z-index: -1;
}

pic.1708406621323

.child 跑到了上面,所以层叠上下文的 background/border < z-index 负值成立。

例子 8:元素的 opacity 值不为 1

1
2
3
4
5
<div class="box">
<div>
<img src="../../images/js1.jpg" alt="" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
.box {
}
.box div {
width: 300px;
background: #168bf5;
}
div img {
width: 200px;
height: 200px;
position: relative;
z-index: -1;
}

pic.1708406632213
因为 z-index: 负值 < 块元素,所以图片在蓝色背景下。然后我们给 div 添加 opacity: 0.5,其余不变

1
2
3
4
5
<div class="box">
<div>
<img src="../../images/js1.jpg" alt="" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
}
.box div {
width: 300px;
background: #168bf5;
opacity: 0.5;
}
div img {
width: 200px;
height: 200px;
position: relative;
z-index: -1;
}

pic.1708406639748

添加 opacity 后,div 变成了层叠上下文元素,图片跑到了上面,所以层叠上下文的 background/border < z-index 负值成立。

例子 9:元素的 filter 属性值不是 none
在例子 8 上,我们给 div 添加 filter: blur(5px),其余不变

1
2
3
4
5
<div class="box">
<div>
<img src="../../images/js1.jpg" alt="" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
}
.box div {
width: 300px;
background: #168bf5;
filter: blur(5px);
}
div img {
width: 200px;
height: 200px;
position: relative;
z-index: -1;
}

pic.1708406650744

同理,结论成立。

例子 10:元素的 transform 属性值不是 none
在例子 8 上,我们给 div 添加 transform: rotate(45deg),其余不变

1
2
3
4
5
<div class="box">
<div>
<img src="../../images/js1.jpg" alt="" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
}
.box div {
width: 300px;
background: #168bf5;
transform: rotate(45deg);
}
div img {
width: 200px;
height: 200px;
position: relative;
z-index: -1;
}

pic.1708406659567

同理,结论成立。

开发调试工具
可以用过 layers 来查看页面的层叠布局,检查遮挡关系
More tools -> Layers
pic.1708406666079

总结

  1. z 轴的存在,其实是网页性能优化的一种手段。使不变的元素保持不变,减少回流。
  2. 因为浏览器优先渲染层叠等级低的元素,所以编码时,我们应该自然而然的把层叠等级低的元素写在前面,让渲染更为流畅。
  3. 查看层叠等级时,先查看是否在同个层叠上下文下,如果是,层叠等级越高越在上面,层叠等级和层叠顺序相同时,后来居上。如果不是,那么判断所处层叠上下文的等级。
  4. 避免滥用 z-index。现在我们知道 inline > float,那么我们只需要把元素变成行内元素即可覆盖在浮动元素之上。过渡使用定位元素会使页面变复杂。
  5. 定位或者 css3 属性 display: flex | inline-flex 下,z-index: auto 所在的元素是普通元素,z-index: 0 是层叠元素。

浏览器如何渲染页面

在地址栏输入 url 后发生了什么?

用一张图描述一下:
pic.1708326672310

  1. 输入域名
  2. DNS:域名解析,得到 ip 地址
  3. 建立 TCP 连接
  4. 客户端发起请求
  5. 服务端处理请求,返回数据
  6. 断开 TCP 连接
  7. 浏览器拿到请求结果,解析 HTML,渲染页面

优化方案里,和日常开发息息相关的是最后的流程,浏览器解析 html,渲染页面。本文主要介绍这一块内容。

网页进程:浏览器网页的渲染和 JS 执行在一个单独的进程中执行。这个进程也称为render 进程。每启动一个页面,都会启动一个 render 进程。

说到网页进程,又引入了线程的概念。

如果说进程是一个工厂,那线程就是工厂中的流水线。一个工厂的正常运行,往往需要多个流水线通力合作才能完成。在网页进程中也是一样,想要网页进程能正常渲染运行,也需要多个线程参与合作。

网页的渲染运行,有如下线程参与

GUI 渲染线程

GUI(Graphical User Interface)表示图形用户界面的意思,render tree 的渲染。用一张图看一下渲染路径

渲染路径

pic.1708326684005

后端返回的 html 文件其内容是个字符串,<html>、<style>等标签是语法糖,HTML Parser 或 CSS Parser 表示对应的解析器,解析器的工作就是将这些字符串语法糖转换为对象。

加载:表示『资源文件发起请求->服务器返回结果』这一过程
解析:表示词法分析,先加载,后解析
渲染:将节点信息绘制到页面的过程

html 的解析过程,是自上而下的,解析过程中,如果发现了样式文件(link 标签),就会加载样式文件。样式文件的加载并不会阻塞 html 的解析,阻塞的是 render tree 的渲染。

HTML 会解析出 DOM(Document Object Model)树,样式文件会解析出 CSSOM(CSS Object Model)树,在 Attachment 环节,GUI 线程将 DOM 树与 CSSOM 树合并在一起,生成渲染树(render tree),并将渲染树绘制到页面。

为什么样式文件的加载会阻塞 render tree 的渲染?

如果不会阻塞,页面会先出现没有样式的 DOM 结构,等 CSSOM 树生成后,需要重新计算 Render Tree,这会造成没必要的损耗。

用代码证明 css 的阻塞现象:
我们先把下载速度设置为 20kbit/s
开发者工具-> Network -> No throttling -> 添加一个 20kbit/s,再观察结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<title>css阻塞</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
h1 {
color: red !important;
}
</style>
<script>
function h() {
// 打印结果不是空数组(断点看结果),说明h1已解析完成。所以证明css的加载不会阻塞HTML的解析
console.log('h1', document.querySelectorAll('h1'));
}
setTimeout(h, 0);
</script>
<link
href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css"
rel="stylesheet"
/>
</head>
<body>
<!-- 样式文件加载完毕之前,h1并没有显示在页面上。所以证明css的加载会阻塞DOM树的渲染 -->
<h1>这是红色的</h1>
</body>
</html>

怎么解决 css 阻塞的阻塞现象?我们可以提高 css 的加载速度:

  1. 使用 CDN。CDN 会替你挑选最近的节点为你提供资源。
  2. 对 css 进行压缩。webpack,gulp 等打包工具
  3. 将多个 css 文件合并。

思考:在页面中新增一个弹窗节点,加在哪个位置最好?页面是如何重新渲染的?
加在 DOM 结构末尾。无论放在哪个位置,整个 HTML 都会重新解析,加在末尾是为了减少生成 render tree 时的计算过程(整颗新旧树还是会比较的,react diff 也会同理)

JS 与渲染路径

js 与样式文件:
解析 HTML 时,如果在 script 元素之前发现样式文件,样式文件的加载和解析会阻塞 JS 的解析和执行

用代码证明 css 的加载阻塞 js 的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<title>css阻塞js的执行</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
console.log('before css');
var startDate = new Date();
</script>
<link
href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css"
rel="stylesheet"
/>
</head>
<body>
<h1>css阻塞js的执行</h1>
<script>
var endDate = new Date();
console.log('after css');
console.log('经过了' + (endDate - startDate) + 'ms');
</script>
</body>
</html>

pic.1708326697875
可以看出,bootstrap 样式文件加载了 111964ms,加载完成后才执行了后面的 JS。

JS 与 HTML,用一张图来表示:
pic.1708326704654
稍微解释一下这张图:
解析 HTML 时,如果发现了 script 元素,JS 的加载和解析都会阻塞 HTML 的解析,等 script 加载以及执行完毕再解析 HTML。这就是我们将 script 放在 DOM 结构后面的原因。

那是不是说 script 标签必须放在 DOM 结构后面?并不是,因为可以给 script 标签添加 async 或 defre 属性。

带 async 属性的 script:

async 表示异步,即用异步的方式加载脚本,render 进程会开启一个新的线程来加载 JS,所以JS 的加载不会阻塞 HTML 的解析,但 JS 的执行仍然会阻塞 HTML 的解析。等 JS 执行完毕后,才继续解析 HTML。

因为 async 加载完成之后会马上执行,所以它是无序的。

带 defer 属性的 script:

defer 表示延迟,即延迟执行,像 async 一样,JS 的加载不会阻塞 HTML 的解析并且 JS 会在 HTML 解析完成后再执行

async 与 defer 的区别:

  • async 脚本是无序的,适合‘完全不依赖它或它不被任何脚本依赖’的脚本
  • async 脚本的执行会阻止 HTML 的解析,defer 不会

async 与 defer 的相同点:

  • 使用 async 或 defer 属性,render 进程会开启一个新的线程来加载 JS 文件,也就是说 script 的加载是和 HTML 的解析同时进行的

HTML 的解析能不能和 JS 的执行同时进行?
不能。GUI 线程负责 HTML 的解析,JS 引擎线程负责 JS 的执行,虽然是两个线程,但他们是互斥的。

补充:
DOMContentLoaded
HTML 解析完成后,DOMContentLoaded 就会触发,解析完成表示我们已经可以访问页面的 DOM 元素了。DOMContentLoaded 类似于 JQuery 中的$(document).ready(function() { // ...代码... })

load
整个页面及所有依赖资源如图片和样式表都已加载完成时触发。
DOMContentLoaded 时间会比 load 小,时间差表示依赖资源加载的时间。

参考:
https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html
https://segmentfault.com/q/1010000000640869
https://www.cnblogs.com/caizhenbo/p/6679478.html

在页面的交互过程中,需要重绘或者由于某些操作引发了回流时,GUI 会重新计算 render tree,然后重新绘制页面元素。

重绘和回流

重绘(repaint):当页面元素的样式发生改变时,GUI 会根据新样式重新绘制该元素,这个过程称为重绘。重绘不影响布局

导致重绘的操作有哪些:

  • 改变元素的外观,可见性(visibility)
  • 回流导致重绘

利用 chrome 中的 Paint flashing 工具,可以观察元素的重绘。
开发者工具 -> more tools -> Rendering,勾选 Paint flashing。重绘的元素会高亮显示。

回流(reflow):当页面元素的尺寸、结构发生改变时,GUI 重新计算的过程,这个过程称为回流。回流完成后,重新绘制受影响的部分到屏幕中,所以说回流必将引起重绘。

导致回流的操作有哪些:

  • 浏览器窗口发生变化
  • 元素的尺寸、位置发生变化
  • 元素内容、字体大小发生变化
  • 激活 css 伪类,例如 hover
  • 添加或者删除元素
  • 滚动

如何减少回流

抓住一个核心原则,对于 DOM 元素的操作,避免影响该元素之外的元素

  • 如果想要改变元素样式,可以通过 class 名,而不是使用 JS 操作
  • 避免使用多层内联样式
  • 首屏服务端渲染,减少页面内容计算次数
  • 避免使用 table 布局

JS 引擎线程

浏览器是不支持直接运行 JS 代码的,所以需要在浏览器中植入一个内核,来支持 JS 的解析和运行。在 chrome 中,这个内核叫 V8。

一个网页只会启动一个 JS 线程来处理 JS 脚本。

JS 线程是单线程。这点不难理解,如果是多线程,一个线程删除 DOM,一个线程新增 DOM,浏览器要如何处理呢?

还有一点,JS 线程和 GUI 线程是互斥的。所以在渲染路径阶段,存在阻塞问题。

定时触发器线程

专门负责 setTimeout/setInterval 的逻辑。应该结合事件循环中的队列来理解定时器线程的执行过程。

事件触发线程

当我们鼠标点击与滑动、键盘的输入等都会触发一些事件,而这些事件的触发逻辑的处理,就是依靠事件触发线程来帮助浏览器完成。

该线程也会把事件的逻辑放入队列中,等待 JS 引擎的处理。在事件循环中,事件触发为宏任务。

http 线程

使用无状态短链接的 http 请求,在应用层基于 http 协议的基础之上,达到与服务端进行通信的目的。

该线程的触发罗家,不是在 JS 引擎线程之中,过程是异步的。

参考:
https://juejin.cn/post/6844903667733118983
https://juejin.cn/post/6844903667733118983

浏览器的进程与线程

什么是进程

操作系统上有很多 app,app 就是应用程序,应用程序就是一个软件包,一个代码集合。而应用程序是静态的,想要运行应用程序,操作系统就会对应的启动一个进程,负责该程序的运行。

把浏览器看做一个大集团,那进程就是集团下的某一个工厂。

总结进程有以下特点:

  • 动态性:进程的实质是程序的一次执行过程
  • 并发性:集团可以有多个工厂,浏览器也可以同时运行多个进程
  • 独立性:工厂是独立的,进程是一个能独立运行的基本单位
  • 异步性:因为是独立的,因此可以按照自己逻辑不可预知的运行
  • 协同性:应用程序之间能够协作完成一些任务

浏览器进程

与操作系统一样,浏览器也有一个任务管理器,每一个任务,都是独立的进程。
pic.1708326642981

浏览器主要包括如下进程:

一、浏览器主进程

浏览器只会创建一个主进程,它主要负责:

  • 浏览器界面提供的交互,如前进、后退、标签栏、设置等
  • 各页面进程的管理,如创建、关闭等
  • 网络资源的管理,如下载内容、缓存等

二、网页进程

网页进程:浏览器网页的渲染和 JS 执行在一个单独的进程中执行。这个进程也称为render 进程。每启动一个页面,都会启动一个 render 进程。

说到网页进程,又引入了线程的概念。

如果说进程是一个工厂,那线程就是工厂中的流水线。一个工厂的正常运行,往往需要多个流水线通力合作才能完成。在网页进程中也是一样,想要网页进程能正常渲染运行,也需要多个线程参与合作。

网页的渲染运行,有如下线程参

GUI 渲染线程

GUI(Graphical User Interface)表示图形用户界面的意思,render tree 的渲染。用一张图看一下渲染路径
pic.1708326653199
HTML 会解析出 DOM(Document Object Model)树,样式文件会解析出 CSSOM(CSS Object Model)树,在 Attachment 环节,GUI 线程将 DOM 树与 CSSOM 树合并在一起,生成渲染树(render tree),并将渲染树绘制到页面。

JS 引擎线程

浏览器是不支持直接运行 JS 代码的,所以需要在浏览器中植入一个内核,来支持 JS 的解析和运行。在 chrome 中,这个内核叫 V8。

一个网页只会启动一个 JS 线程来处理 JS 脚本。

JS 线程是单线程。这点不难理解,如果是多线程,一个线程删除 DOM,一个线程新增 DOM,浏览器要如何处理呢?

还有一点,JS 线程和 GUI 线程是互斥的。所以在渲染路径阶段,存在阻塞问题。

定时触发器线程

专门负责 setTimeout/setInterval 的逻辑。应该结合事件循环中的队列来理解定时器线程的执行过程。

事件触发线程

当我们鼠标点击与滑动、键盘的输入等都会触发一些事件,而这些事件的触发逻辑的处理,就是依靠事件触发线程来帮助浏览器完成。

该线程也会把事件的逻辑放入队列中,等待 JS 引擎的处理。在事件循环中,事件触发为宏任务。

http 线程

使用无状态短链接的 http 请求,在应用层基于 http 协议的基础之上,达到与服务端进行通信的目的。

该线程的触发罗家,不是在 JS 引擎线程之中,过程是异步的。

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×