前言
这篇文章将会一起复习 css 选择器知识 , 理论与实践相结合 , 我们将会一起卷:
- css 选择器&渲染引擎
- 选择器分类与权重
- 伪类和伪元素进阶
- 属性选择器模式匹配
- 优先级冲突
一、选择器底层实现原理
我们首先来看看选择器的底层实现 , 因为只有明白了上下游 , 在上下游中定位了选择器的位置 , 我们可以更好的理解{CSS 的选择器} , 所以一起探讨:
- 渲染引擎的工作流程 ?
- 渲染过程中 , 样式是如何匹配的 ?
渲染引擎工作流程
看下图 , 清晰的展现了渲染引擎的工作流程 , 接下来 , 对这张图逐帧学习🤡
1.1.1 解析阶段
1)HTML 解析
浏览器将 HTML 字符串解析为DOM树
(Document Object Model),过程如下:
- 词法分析 :将 HTML 文本分割为标签、属性等 Token(如
<div>
、class="box"
)。 - 语法分析 :将 Token 转换为节点对象(如
Element
、Text
),并构建树状结构。
特点 :解析是渐进式的,边下载边解析,无需等待整个文档加载完成。
2)CSS 解析
浏览器将 CSS 文本解析为CSSOM树
(CSS Object Model),过程类似 DOM 解析:
- 解析外部 CSS 文件(如
<link>
标签)或内联样式(如<style>
标签)。 - 合并用户代理默认样式(如浏览器内置的
user agent styles
)。
特点 :CSSOM 构建必须等待完整 CSS 文件加载完成,因为后续规则可能覆盖前面的样式。
1.1.2 构建 Render Tree
接下来构建渲染树(Render Tree)
在渲染树中 , 其实还有更详细的流程 , 我们展开如下图 :
-
合并 DOM 和 CSSOM :
浏览器遍历 DOM 树,为每个可见节点(如
display: none
的节点会被忽略)匹配 CSS 规则,生成Render Tree
。 -
样式匹配算法:
- 从右到左匹配 :现代浏览器(如 WebKit、Blink)采用 BFS 算法,优先匹配最右边的选择器(如
.box p
先匹配p
,再向上查找.box
),减少无效遍历。 - 缓存机制:缓存常用选择器的匹配结果,避免重复计算。
- 从右到左匹配 :现代浏览器(如 WebKit、Blink)采用 BFS 算法,优先匹配最右边的选择器(如
-
冲突解决 :
根据 CSS 优先级(
!important
> ID > 类 > 标签)和层叠规则(后定义的样式覆盖先定义的)确定最终样式。
看到这里可以知道知道 CSS 选择器 , 他在渲染过程中 , 处于什么阶段了吗 ?,在构建 Render Tree 过程中 !样式匹配的时候!
接下来继续讲解这张图 , 也就是下游 ~
1.1.3 布局(Layout)
- 计算几何信息 :
确定每个节点在视口(Viewport)中的位置和尺寸,生成布局树
(Layout Tree)。 - 盒模型计算 :
应用盒模型规则(如width
、margin
、float
),处理文档流、定位和 Flexbox/Grid 布局。 - 触发条件 :
当 DOM 或样式变化时(如元素增删、窗口缩放),会触发重排
(Reflow),导致布局重新计算。
1.1.4 绘制(Paint)
- 将布局转换为像素 :
遍历布局树,将每个节点的样式(如颜色、边框、阴影)绘制为像素,生成绘制记录
(Paint Record)。 - 分层优化 :
复杂场景(如 3D 变换、透明度)会被分层处理,减少重复绘制。例如,position: fixed
元素单独成层。 - 触发条件 :
样式变化(如背景色修改)会触发重绘
(Repaint)。
绘制之后 ,进行合成 ~
1.1.5 合成(Composite)
主要有一下几个步骤
- 生成最终图像 :
将绘制的层按照顺序合并(Compositing),应用变换、透明度等效果,输出到屏幕。 - GPU 加速 :
现代浏览器使用 GPU 处理合成,提高复杂动画和滚动的流畅度。 - 优化策略 :
只更新变化的层(如滚动时仅重绘视口内区域),减少计算量。
通过上述渲染引擎的工作流程 ,了解 css 选择器在渲染引擎中的底层作用 , 现在我们继续讨论 css 的选择器 ~
二、选择器分类与权重
2.1 优先级计算表
我们直接用表格的形式 , 按照选择器的权重排序快速展现
选择器类型 | 权重值 | 示例 | 匹配范围 | 详细说明 |
---|---|---|---|---|
内联样式(!important) | 10000 | <p style="color: red;"> |
单个元素 | 直接写在 HTML 标签中的样式,带有 !important 声明时,具有最高优先级,会覆盖其他任何样式规则。不过,应谨慎使用 !important ,因为它会破坏样式表的层叠规则,使代码难以维护。 |
ID 选择器 | 100 | #main |
唯一元素 | 通过元素的 id 属性来选择元素,由于 id 在 HTML 文档中必须是唯一的,所以该选择器能精准定位到单个元素。 |
类选择器 | 10 | .card |
多个元素 | 选择具有指定类名的所有元素。类名可以在多个元素上重复使用,因此适合用于批量设置样式。 |
属性选择器 | 10 | [type="email"] |
符合条件的元素 | 根据元素的属性及其值来选择元素。可以选择具有特定属性的元素,或者属性值满足特定条件的元素。 |
伪类选择器 | 10 | :hover |
动态状态元素 | 用于选择处于特定状态的元素,如鼠标悬停(:hover )、链接已访问(:visited )、元素获得焦点(:focus )等。这些状态是动态的,会根据用户的交互而改变。 |
元素选择器 | 1 | p |
所有同类元素 | 选择 HTML 文档中所有指定类型的元素,如所有的 <p> 标签、所有的 <div> 标签等。 |
通配符选择器 | 0 | * |
全部元素 | 选择 HTML 文档中的所有元素。通常用于全局设置样式,如重置默认边距和内边距。 |
伪元素选择器 | 1 | ::before |
元素前后内容 | 用于选择元素的特定部分,如元素的前面(::before )或后面(::after )插入的内容。伪元素实际上并不存在于 HTML 文档中,而是由 CSS 动态生成的。 |
否定伪类选择器 | 10 | :not(p) |
除指定元素外的其他元素 | 选择不匹配指定选择器的所有元素。例如,:not(p) 会选择除 <p> 标签之外的所有元素。 |
相邻兄弟选择器 | 组合权重 | h1 + p |
紧跟在指定元素后的相邻兄弟元素 | 选择紧跟在指定元素后面的具有相同父元素的相邻兄弟元素。例如,h1 + p 会选择紧跟在 <h1> 标签后面的 <p> 标签。 |
通用兄弟选择器 | 组合权重 | h1 ~ p |
指定元素后面的所有兄弟元素 | 选择指定元素后面的具有相同父元素的所有兄弟元素。例如,h1 ~ p 会选择 <h1> 标签后面的所有 <p> 标签。 |
子选择器 | 组合权重 | ul > li |
指定元素的直接子元素 | 选择指定元素的直接子元素。例如,ul > li 会选择 <ul> 标签的直接子元素 <li> 标签。 |
后代选择器 | 组合权重 | div p |
指定元素的所有后代元素 | 选择指定元素的所有后代元素,包括子元素、孙元素等。例如,div p 会选择 <div> 标签内的所有 <p> 标签。 |
我们现在知道了各个选择器的权重值,那么复合的选择器的权重怎么计算呢 ?
下面 , 我举一些例子来探讨一下 ~
2.1.1 复合选择器权重计算
复合选择器由多个简单选择器组合而成,其权重值是各个简单选择器权重值的总和。以下是更多复合选择器权重计算的示例:
css
/* 权重计算示例 */
#header .nav > li.active a:hover {
/* 100 (ID) + 10 (类) + 1 (元素) + 10 (类) + 1 (元素) + 10 (伪类) = 132 */
color: #e74c3c;
}
body #content .article h2::first-letter {
/* 1 (元素) + 100 (ID) + 10 (类) + 1 (元素) + 1 (伪元素) = 113 */
font-size: 2em;
}
input[type="text"]:focus {
/* 1 (元素) + 10 (属性选择器) + 10 (伪类) = 21 */
border-color: blue;
}
section:not(.special) p {
/* 1 (元素) + 10 (否定伪类) + 1 (元素) = 12 */
color: green;
}
在实际应用中,当多个样式规则应用到同一个元素时,浏览器会根据选择器的权重来决定使用哪个样式。权重值越高的选择器,其样式规则优先级越高。如果多个选择器的权重值相同,则后定义的样式规则会覆盖先定义的样式规则。
我们举个例子
你正在开发一个博客网站,页面结构包含头部导航栏、文章列表以及页脚。在设计样式时,你需要针对不同部分的元素应用不同的样式,同时要处理好复合选择器的权重问题,以确保样式能够按照预期生效。
HTML 结构
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>博客网站</title>
</head>
<body>
<!-- 头部导航栏 -->
<header id="main-header">
<nav class="nav-menu">
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">文章</a></li>
<li><a href="#">关于</a></li>
</ul>
</nav>
</header>
<!-- 文章列表 -->
<main id="article-list">
<article class="article-item">
<h2>文章标题 1</h2>
<p>文章内容 1...</p>
<a href="#" class="read-more">阅读更多</a>
</article>
<article class="article-item">
<h2>文章标题 2</h2>
<p>文章内容 2...</p>
<a href="#" class="read-more">阅读更多</a>
</article>
</main>
<!-- 页脚 -->
<footer id="main-footer">
<p>版权所有 © 2025 博客网站</p>
</footer>
</body>
</html>
CSS 样式及权重计算分析
css
/* 导航栏链接样式 */
#main-header .nav-menu ul li a {
/* 权重计算:
#main-header 是 ID 选择器,权重为 100
.nav-menu 是类选择器,权重为 10
ul 是元素选择器,权重为 1
li 是元素选择器,权重为 1
a 是元素选择器,权重为 1
总权重 = 100 + 10 + 1 + 1 + 1 = 113
*/
color: #333;
text-decoration: none;
}
/* 文章标题样式 */
#article-list .article-item h2 {
/* 权重计算:
#article-list 是 ID 选择器,权重为 100
.article-item 是类选择器,权重为 10
h2 是元素选择器,权重为 1
总权重 = 100 + 10 + 1 = 111
*/
color: #e74c3c;
}
/* 文章阅读更多链接样式 */
#article-list .article-item a.read-more {
/* 权重计算:
#article-list 是 ID 选择器,权重为 100
.article-item 是类选择器,权重为 10
a 是元素选择器,权重为 1
.read-more 是类选择器,权重为 10
总权重 = 100 + 10 + 1 + 10 = 121
*/
color: #2980b9;
text-decoration: underline;
}
/* 页脚段落样式 */
#main-footer p {
/* 权重计算:
#main-footer 是 ID 选择器,权重为 100
p 是元素选择器,权重为 1
总权重 = 100 + 1 = 101
*/
color: #7f8c8d;
text-align: center;
}
分析说明
- 导航栏链接样式:借助复合选择器精准定位到导航栏里的链接元素,总权重为 113。
- 文章标题样式:能够准确选中文章列表中的标题元素,总权重为 111。
- 文章阅读更多链接样式:专门针对文章中的"阅读更多"链接,由于其权重为 121,高于其他链接选择器,所以能保证该样式正确应用。
- 页脚段落样式:可选中页脚的段落元素,总权重为 101。
通过对复合选择器权重的合理计算和运用,你可以确保每个元素都能应用到预期的样式,避免样式冲突的问题。
三、伪类和伪元素进阶
3.1 结构伪类进阶
3.1.1 奇偶行交替
css
/* 选择 tbody 内的偶数行 */
tbody tr:nth-child(even) {
background-color: #b31635;
}
/* 选择 tbody 内的奇数行 */
tbody tr:nth-child(odd) {
background-color: #199523;
}
在浏览器中打开该 HTML 文件,表格的表头不会受奇偶行变色样式的影响,而表格主体部分的奇数行背景颜色会显示为 #199523
(绿色),偶数行背景颜色会显示为 #b31635
(红色)。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>奇偶行交替表格</title>
<style>
// 放这里
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
<tr>
<td>张三</td>
<td>25</td>
</tr>
<tr>
<td>李四</td>
<td>30</td>
</tr>
<tr>
<td>王五</td>
<td>22</td>
</tr>
</tbody>
</table>
</body>
</html>
3.1.2 分页控制
css
/* 显示第2页内容 */
.page-content:target {
display: block;
}
.page-content {
display: none;
}
当你点击导航链接时,URL 会发生变化,例如点击 "页面 2" 链接,URL 会变为 yourpage.html#page2
,此时页面 2 的内容就会显示出来,其他页面内容则保持隐藏。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分页内容显示</title>
<style>
// 放这里
</style>
</head>
<body>
<!-- 导航链接 -->
<ul>
<li><a href="#page1">页面 1</a></li>
<li><a href="#page2">页面 2</a></li>
<li><a href="#page3">页面 3</a></li>
</ul>
<!-- 页面内容 -->
<div id="page1" class="page-content">
<h2>页面 1</h2>
<p>这是页面 1 的内容。这里可以展示各种与页面 1 相关的信息,比如产品介绍、新闻资讯等。</p>
</div>
<div id="page2" class="page-content">
<h2>页面 2</h2>
<p>这是页面 2 的内容。页面 2 可能会有不同的主题,例如技术文章、案例分析等。</p>
</div>
<div id="page3" class="page-content">
<h2>页面 3</h2>
<p>这是页面 3 的内容。页面 3 或许会呈现一些娱乐内容、用户评价等。</p>
</div>
</body>
</html>
3.2 伪元素高级用法
3.2.1 动态内容生成
css
/* 自动添加版权信息 */
body::after {
content: "Copyright " attr(data-year);
display: block;
text-align: center;
padding: 20px;
background-color: #f8f9fa;
}
这里我添加了动画 , 为了突出自动生成 , 上图是我刷新页面后的结果
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自动添加版权信息</title>
<style>
body::after {
content: "自动添加 - Copyright " attr(data-year);
display: block;
text-align: center;
padding: 20px;
background-color: #222;
color: white;
font-size: 18px;
border-top: 2px solid #ccc;
position: fixed;
bottom: -100px;
left: 0;
right: 0;
opacity: 0;
animation: slideUp 1s ease-out 0.5s forwards;
}
@keyframes slideUp {
to {
bottom: 0;
opacity: 1;
}
}
</style>
</head>
<body data-year="2025">
<h1>欢迎访问我的网页</h1>
<p>这是网页的主要内容,你可以在这里展示各种信息。</p>
</body>
</html>
我们分析一下
content
属性:
content
属性是body::after
中最为关键的属性,它用于定义要插入的内容。在上述示例里,content: "Copyright " attr(data-year);
意味着会插入一段文本"Copyright "
,接着通过attr(data-year)
函数获取body
元素的data-year
属性值。所以,最终插入的内容是"Copyright 2025"
。attr()
函数能够获取 HTML 元素的属性值,这让插入的内容更具动态性。
display
属性:
display: block;
把插入的内容作为块级元素来显示,这表明该内容会独占一行,并且可以设置宽度、高度、内边距等属性。
text-align
属性:
text-align: center;
让插入的内容在水平方向上居中显示。
padding
属性:
padding: 20px;
为插入的内容添加了 20 像素的内边距,使内容与周围元素之间有一定的间隔。
background-color
属性:
background-color: #f8f9fa;
为插入的内容设置了背景颜色,方便与页面的其他部分区分开来。
自动添加的过程
- 当浏览器渲染页面时,会依据 CSS 规则对页面元素进行样式设置。在处理
body
元素时,会检测到body::after
伪元素的规则。 - 接着,浏览器会根据
content
属性的定义,从body
元素的data-year
属性中获取值,然后将组合好的内容插入到body
元素的末尾。 - 最后,按照其他 CSS 属性(如
display
、text-align
、padding
、background-color
等)对插入的内容进行样式设置,从而实现版权信息的自动添加。
3.2.2 文本截断
我写过一篇文章 , 可以看看 :
css
/* 多行文本截断 */
.text-truncate {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
四、属性选择器模式匹配
4.1 表单验证
借助属性选择器和 :valid
、:invalid
伪类对密码输入框进行验证,依据输入内容是否符合规则展示不同样式。
css
/* 强密码验证 */
input[type="password"]:valid {
border-color: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.1);
}
input[type="password"]:invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
input[type="password"]:valid
:当密码输入框内的内容符合验证规则(这里要求必填且长度至少为 6 个字符)时,输入框的边框颜色变为绿色(#22c55e
),同时添加绿色的阴影效果。input[type="password"]:invalid
:当密码输入框内的内容不符合验证规则时,输入框的边框颜色变为红色(#ef4444
),并添加红色的阴影效果。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表单密码验证</title>
<style>
input[type="password"]:valid {
border-color: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.1);
}
input[type="password"]:invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
</style>
</head>
<body>
<form>
<label for="password">请输入密码:</label>
<input type="password" id="password" required minlength="6">
<input type="submit" value="提交">
</form>
</body>
</html>
4.2 图标字体
通过属性选择器和伪元素 ::before
依据元素的 icon
属性动态显示不同的图标。
css
/* 动态图标映射 */
[icon="home"]::before {
font-family: 'Material Icons';
content: "🏠";
}
[icon="search"]::before {
content: "🔍";
}
[icon="home"]::before
:当元素的icon
属性值为"home"
时,在元素内容(主页)之前插入一个房屋图标(🏠
)。[icon="search"]::before
:当元素的icon
属性值为"search"
时,在元素内容(搜索)之前插入一个搜索图标(🔍
)。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图标字体动态映射</title>
<style>
[icon="home"]::before {
font-family: 'Material Icons';
content: "🏠";
}
[icon="search"]::before {
content: "🔍";
}
</style>
</head>
<body>
<span icon="home">主页</span>
<span icon="search">搜索</span>
</body>
</html>
五、优先级冲突解决方案
5.1 权重提升策略
css
/* 最高优先级 */
.urgent {
color: #e74c3c !important;
}
/* 组合选择器 */
div > ul > li.active {
font-weight: bold;
}
.urgent
类使用了!important
声明,这会让它的color
属性具有最高优先级,即使有其他规则试图覆盖它也不行。div > ul > li.active
是一个组合选择器,它通过指定元素的层级关系,让规则更加具体,因此具有较高的权重。只有同时满足在div
下的ul
中的li
元素且具有active
类的元素才会应用该样式。
5.2 权重可视化工具
javascript
// 自制权重计算器
function calculateSpecificity(selector) {
const pattern = /([#.])(\w+)|(\w+)/g;
let [id, cls, tag] = [0, 0, 0];
selector.replace(pattern, (match, hash, classname, element) => {
if (hash === '#') id++;
if (hash === '.') cls++;
if (element) tag++;
});
return {
specificity: `${id},${cls},${tag}`,
weight: id * 100 + cls * 10 + tag
};
}
// 使用示例
console.log(calculateSpecificity('#header .nav > li.active'));
// 输出: { specificity: "1,2,1", weight: 121 }
calculateSpecificity
函数接受一个 CSS 选择器作为参数。- 使用正则表达式
/([#.])(\w+)|(\w+)/g
来匹配选择器中的id
、类和标签。 - 通过
replace
方法遍历匹配结果,根据不同的匹配情况增加id
、cls
和tag
的计数。 - 最后返回一个对象,包含选择器的特异性(格式为
id,cls,tag
)和计算得到的权重值。权重值的计算规则是id * 100 + cls * 10 + tag
,这是因为id
的权重最高,类其次,标签最低。
总结
点个赞吧 ~