源自于项目中想要修改 elemenplus
组件库的样式,以符合 UI 设计的要求,但发现自己并不是十分理解,结合源码来梳理相关的内容。
需要提前了解的知识点
属性选择器
CSS 属性选择器是**基于元素的属性和属性值对 dom 元素进行匹配**,从宽泛的角度来说** id 选择器和类选择器也属于属性选择器**。基础语法如下:
[属性名] {
/* CSS 规则 */
}
/* 选择具有特定属性值的元素 */
[属性名="属性值"] {
/* CSS 规则 */
}
选择器的优先级
我们知道 css 中有 id 选择器、类选择器、元素选择器等等,当一个元素同时被多个选择器匹配的时候,元素最终的样式取决于选择器的优先级,优先级的计算是有一套规则的:我们将其划分为0~4共5个等级,其中前4个等级由CSS选择器决定,最后一个等级由书写形式决定级别 | 选择器类型 | 权重值 | 示例 |
---|---|---|---|
0级 | 通配选择器、选择符和逻辑组合伪类 | 0 | * 、:not() 、:is() 、:where() |
1级 | 标签选择器 | 1 | #foo { color: #999; } |
2级 | 类选择器、属性选择器和伪类 | 10 | .foo { color: #666; } [foo] { color: #666; } :hover { color: #333; } |
3级 | ID 选择器 | 100 | |
4级 | style 属性内联 | 1000 |
我们来看几个例子理解优先级权重的计算:
选择器 | 优先级数值 | 计算规则 |
---|---|---|
*{} | 0 | 1个0级通配选择器, 优先级数值为0 |
dialog{} | 1 | 1个1级标签选择器, 优先级数值为1 |
a:hover{} | 11 | 1个1级标签选择器, 1个2级伪类, 优先级数值为1+10 |
#foo{} | 100 | 1个3级ID选择器, 优先级数值为100 |
#foo.bar p{} | 111 | 1个3级ID选择器, 1个2级类选择器, 1个1级标签选择器, 优先级数值为100+10+1 |
!important 作用的底层机制
通常我们认为CSS的优先级就像一个小世界,设置了`!important`之后,这个CSS属性就可以在CSS世界中“称王称霸”。实际上,`!important`所起的作用不是这样,而是直接将这个CSS属性带到了一个更高级别的级联层级。CSS的优先级是分层的,就像一层层的建筑,每一层就像一个封闭的小宇宙,与其他层互不关联,其优先级也无法超越。
级联层级的优先级关系:
- 设置了
!important
的浏览器内置样式; - 设置了
!important
的用户设置的样式; @layer
规则中设置的包含!important
的样式;- 开发者设置的包含
!important
的样式; - 开发者设置的CSS样式;
@layer
规则中的CSS样式;- 用户设置的CSS样式;
- 浏览器内置的CSS样式。
这个优先级顺序也很自然,给了开发者和用户修改样式的自由。
scoped 实现基础原理
vue 中 `scoped` 的实现的原理是:- 对给
style
标签添加了scoped
属性的组件的 dom 元素添加上唯一的 id 标识; - 并给 style 中每一个选择器末尾都添加上这个 id 标识的属性选择器,也因此,使用
scoped
后style
标签中声明的样式只会影响到该组件,而不会影响其他组件或子组件。
我们来看看这段 vue 简单的代码:
<template>
<div class="foo">
<p class="bar">Hello, world!</p>
</div>
</template>
<style scoped>
.foo {
background-color: blue;
}
.bar {
color: red;
}
</style>
vue 的单文件组件编译器(vue-compile-sfc)
会把它编译 为类似 下边的代码:
<template>
<div class="foo">
<p class="bar">Hello, world!</p>
</div>
</template>
<style scoped>
.foo {
background-color: blue;
}
.bar {
color: red;
}
</style>
对子组件的处理
如果父组件和子组件都在`style`中声明了`scoped`,则**子组件的根节点**会包含两个 id 标识符`(data-v-xxxx)`::deep 做了什么
:deep 其实是深度选择器,用于将父组件中的样式透传影响到子组件。它的实现也很简单,就是将 **id 标识属性选择器**前置,使得父组件的样式能够应用到子组件。通常来说,我们经常会有修改第三方 UI 库的组件样式的需求,在我们组件中引入的第三方组件,也相当于子组件;
看以下例子就可以明白,对于下边的这段 css 代码:
:deep(.foo) {
color: red;
}
.foo :deep(.bar) {
color: green;
}
vue 编译器会将它转换为:
🧐源码探究
我们来看看 vue 源码中对 scoped 和 :deep 语法是怎么处理的:源码中 css 处理是通过一个自定义的 postcss 插件:pluginScoped.ts
,文件位于compile-sfc
包下:
核心处理的代码为:
/**
* 重写选择器,向选择器中注入给定的ID属性。
*
* @param id 要注入的选择器的ID。
* @param selector 当前正在处理的选择器。
* @param selectorRoot 选择器的根节点。
* @param slotted 是否为slotted选择器,默认为false。
*/
function rewriteSelector(
id: string,
selector: selectorParser.Selector,
selectorRoot: selectorParser.Root,
slotted = false
) {
selector.each(n => {
// 其他代码......
if (n.type === 'pseudo') {
const { value } = n
// 核心处理逻辑
// deep: 在 ::v-deep 节点前注入 [id] 属性
if (value === ':deep' || value === '::v-deep') {
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// 如果没有空格组合符,则插入一个
// 用于处理边界条件,比如原始选择器和:deep没有空格:// .foo::v-deep(.bar) // 处理后 .foo[xxxxxxx] .bar
const prev = selector.at(selector.index(n) - 1)
if (!prev || !isSpaceCombinator(prev)) {
selector.insertAfter(
n,
selectorParser.combinator({
value: ' '
})
)
}
selector.removeChild(n)
}
return false
}
}
// 其他代码......
})
// 其他代码......
}