用Vue3写项目时,::v-deep、:deep、v-deep这些写法到底选哪个?会不会有性能坑?
最近刷前端交流群,发现好多从Vue2转过来的朋友还在纠结Vue3的深层样式穿透写法——一会儿看到别人用deep()括号套,一会儿又碰见过时的:v-deep在报错,偶尔还有人用Vue2的/deep/凑活能用又担心项目兼容,其实这三个写法(不算旧版兼容用的/deep/和:v-deep旧格式)的区别、适用场景、性能影响早就有明确的规范,只是很多人没理清,咱们今天就用大白话,把深层样式穿透的前世今生、最佳实践、可能踩的雷全讲透。
先搞懂:为什么Vue3要改深层穿透的写法?
用过Vue2单文件组件(SFC)的人都知道,为了防止组件样式污染全局,我们会在<style>标签上加scoped属性,加了之后,Vue会给组件里所有的DOM元素加一个唯一的data属性(比如data-v-7ba5bd90),同时把对应的CSS选择器改成带这个属性的(比如.btn变成.btn[data-v-7ba5bd90])。
但这个机制有个小bug——如果组件里用了第三方组件,或者是通过插槽传进来的DOM元素,它们的根节点以外的子元素,根本不会带当前组件的唯一data属性!比如你用Element Plus的el-select,组件内部的下拉框、选项都是第三方组件生成的,你的.el-select-dropdown加了scoped根本选不中,这时候就需要“深层穿透”——让当前组件的scoped CSS规则,能“钻”进第三方或插槽子组件的内部去选元素。
那为什么Vue3不保留Vue2的/deep/和:v-deep呢?这里有两个核心原因:
第一,/deep/是Sass/SCSS预处理器的语法冲突写法——早期很多预处理器用/deep/做嵌套选择器的分隔符(后来虽然主流预处理器改了,但历史包袱太重,而且有些旧项目可能还在用旧配置),Vue官方不想和预处理器规则打架;
第二,:v-deep旧格式是CSS伪元素的写法,但它其实是Vue自己的“语法糖占位符”,不符合W3C的CSS规范,未来如果浏览器真的支持了:v-deep这个伪元素,Vue的代码就得大改,所以Vue3从beta版本开始,就主推更规范、更通用的deep()函数式写法了。
现在202X年了,主流写法到底是哪一个?
一句话总结:新项目100%用deep(),不要用其他写法。
咱们先列个清晰的对比表:
| 写法 | Vue2支持 | Vue3支持 | 推荐度 | 适用场景 |
|------|----------|----------|--------|----------|
| /deep/ | ✅(SFC) | ❌(Vite脚手架默认禁用) | ⭐ | 别用,除非你维护的是Vue2+Webpack的极端旧项目 |
| :v-deep | ✅(SFC) | ❌(Vue3.3+彻底移除支持) | ⭐ | 别碰,升级Vue3.3之后直接报红(或者是样式失效,具体看脚手架配置) |
| :v-deep(.class) | ✅(Vue2.7+) | ✅(过渡期) | ⭐⭐ | 过渡期兼容老代码用,但不要写新代码 |
| deep(.class) | ✅(Vue2.7+) | ✅(主推) | ⭐⭐⭐⭐⭐ | 所有场景通用,符合W3C未来规范 |
| slotted(.class) | ❌ | ✅ | ⭐⭐⭐⭐ | 专门用来穿透插槽子元素的,后面单独讲 |
这里要注意两个细节:
第一个是Vue2.7+也支持deep()和slotted()了!如果你正在维护Vue2的老项目,建议先升级到Vue2.7,然后把所有旧的穿透写法都换成新的,这样以后升级Vue3会省很多事;
第二个是过渡期的:v-deep(.class),其实和deep(.class)功能完全一样,但Vue3官方已经明确说了这是“临时兼容语法”,随时可能删除,所以写新代码的时候坚决不用。
别乱用!deep()和slotted()到底有啥区别?
很多朋友可能以为deep()能解决所有scoped穿透的问题,但其实Vue3还专门加了一个slotted(),这俩的作用范围完全不一样,混用的话要么样式没用,要么会污染其他组件。
先讲最常用的deep()
deep()的作用范围是:当前组件内所有包含唯一data属性的DOM元素的子元素——不管这些子元素是第三方组件生成的,还是自己写的嵌套组件生成的,只要父元素(或者祖先元素)带了当前组件的data属性,deep()就能选到它们。
举个例子吧,假设我们写了一个叫MyForm的组件,里面用了Element Plus的el-input:
<template>
<div class="my-form-container">
<el-input v-model="inputVal" class="my-input" />
</div>
</template>
<style scoped>
/* 错误写法:scoped会把.my-input的内部选择器变成.el-input__inner[data-v-xxx],但el-input__inner是第三方组件的子元素,不带data-v-xxx */
.my-input .el-input__inner {
border-color: red;
}
/* 正确写法::deep()会把规则变成.my-input[data-v-xxx] .el-input__inner,这时候就能选中第三方组件的子元素了 */
.my-input :deep(.el-input__inner) {
border-color: red;
}
</style>
这里的my-input-container和my-input的根元素(也就是el-input组件的根DOM)都会带MyForm组件的data属性,所以deep()能“钻”进el-input的根DOM下面,选到el-input__inner。
再讲专门针对插槽的slotted()
那什么时候用slotted()呢?就是当你要选的元素是通过插槽传进来的,而且这些元素的根节点都不带当前组件的data属性的时候。
举个更具体的例子,比如我们写了一个叫MyCard的卡片组件:
<!-- MyCard.vue -->
<template>
<div class="my-card">
<h3 class="card-title">{{ title }}</h3>
<div class="card-content">
<!-- 这里是默认插槽,传进来的元素都不会带MyCard的data属性 -->
<slot></slot>
</div>
</div>
</template>
<style scoped>
/* 错误写法:card-content带MyCard的data属性,但插槽里的p不带,card-content p[data-v-xxx]选不到 */
.card-content p {
color: #666;
}
/* 错误写法::deep()是针对带当前组件data属性的元素的子元素,但插槽里的p的父元素(card-content)虽然带data属性,但p本身不是MyCard渲染的,deep()有时候在某些脚手架里会失效(或者是能生效,但逻辑上不对,而且有性能问题) */
.card-content :deep(p) {
color: #666;
}
/* 正确写法::slotted()专门用来选插槽里的元素,不管这些元素的父元素有没有data属性,Vue会自动把规则变成[data-v-xxx] .card-content ::slotted(p),符合W3C Web Components的规范 */
.card-content :slotted(p) {
color: #666;
}
</style>
<!-- 使用MyCard的组件 -->
<template>
<MyCard title="测试卡片">
<p>这是传进来的段落</p>
</MyCard>
</template>
这里要注意,slotted()只能选插槽里的直接子元素,不能选直接子元素的子元素!如果要选插槽里直接子元素的子元素,得在slotted()里面再加嵌套,但不能再加deep()了——比如slotted(.slot-box .slot-inner)是错的,得写成slotted(.slot-box) :deep(.slot-inner)吗?不对,等下,哦不,直接嵌套就行?等下咱们找个规范的例子验证一下(这里我们引用权威的Vue文档思路哈):其实如果要选插槽里嵌套很深的元素,可以直接在slotted()的选择器里写嵌套,但Vue会把整个选择器的“入口”放在插槽的位置上——比如.card-content :slotted(.slot-box span),会被编译成.card-content[data-v-xxx] .slot-box span?不对不对,应该是和Web Components的:slotted()一样,只能选直接子元素,嵌套的话要用其他方式?哦对,等下我刚才记错了,严格来说Vue3的slotted()是完全遵循Web Components规范的,只能选插槽的直接分发元素,如果要选直接分发元素的子元素,应该让直接分发元素带class,然后用deep()在直接分发元素的class下面写规则。
哦刚才的例子可能有点混乱,咱们重新写一个slotted()和deep()配合的正确例子:
<!-- MyCard.vue -->
<template>
<div class="my-card">
<h3 class="card-title">{{ title }}</h3>
<div class="card-content">
<slot></slot>
</div>
</div>
</template>
<style scoped>
/* 1. 先给插槽的直接分发元素加默认样式,用:slotted() */
.card-content :slotted(.slot-wrapper) {
padding: 12px;
border-radius: 4px;
background-color: #f5f5f5;
}
/* 2. 再给直接分发元素的子元素加样式,用:deep(),因为直接分发元素虽然不带MyCard的data属性,但如果我们在slot外面包一层div(或者用slot本身?不对slot不是DOM元素),哦等下,刚才的第一个例子里,MyCard的card-content是带data属性的,但如果直接分发元素是slot-wrapper,那slot-wrapper的子元素的父元素是slot-wrapper,不带data属性,所以怎么破?哦对了,其实我们可以在使用MyCard的组件里,给slot-wrapper加scoped样式吗?不行,因为slot-wrapper是在MyCard的card-content里面的,使用MyCard的组件的scoped样式选不到?哦不对,使用MyCard的组件的scoped样式,会给它自己渲染的DOM元素加data属性,所以slot-wrapper是使用MyCard的组件渲染的,会带使用组件的data属性,那MyCard的scoped样式怎么选slot-wrapper的子元素?哦这里其实有个更简单的逻辑:如果MyCard的样式只影响自己渲染的DOM,或者第三方组件在MyCard内部渲染的DOM,用`:deep()`;如果MyCard的样式要影响使用方传进来的插槽DOM,应该尽量让使用方自己加样式,或者用CSS变量!哦对,CSS变量才是更优雅的解决方案!刚才的`:slotted()`和`:deep()`配合的场景其实很少,除非是公共组件必须给插槽里的某些特定元素加强制样式。
## 深层穿透会不会有性能坑?怎么优化?
很多朋友可能会担心,用了`:deep()`之后,样式的渲染速度会不会变慢?其实这个担心是有道理的,但只要不是滥用,性能影响可以忽略不计。
那为什么`:deep()`会有性能影响呢?因为加了`:deep()`的CSS规则,浏览器在匹配的时候,会先找带当前组件data属性的祖先元素,然后再往下遍历所有子元素,而普通的scoped CSS规则只需要找带对应data属性的元素就行,遍历范围小很多,如果滥用`:deep()`,比如在一个组件里写了几十个`:deep()`规则,而且每个规则的选择器都嵌套很深(.a .b .c :deep(.d .e .f)`),那浏览器的匹配压力就会变大,尤其是在页面元素很多的情况下。
那怎么优化深层穿透的性能呢?这里有3个实用的小技巧:
### 技巧一:尽量缩小`:deep()`的作用范围
不要把`:deep()`写在全局的选择器下面,比如不要写`:deep(.el-input__inner)`,而是要写在具体的父类下面,.my-input :deep(.el-input__inner)`——这样浏览器在匹配的时候,只会遍历`.my-input`下面的子元素,而不会遍历整个页面的元素。
### 技巧二:尽量用更具体的选择器,不要用通配符
通配符`*`的匹配效率是最低的,所以千万不要写`:deep(*)`,哪怕是写`.my-container :deep(*)`也尽量不要,最好用具体的标签名或者类名,.my-container :deep(.specific-class)`。
### 技巧三:优先用CSS变量代替深层穿透
刚才提到过,CSS变量是更优雅的解决方案——如果只是要改第三方组件的颜色、大小、间距这些可配置的样式,优先用第三方组件提供的CSS变量,没有的话也可以自己给第三方组件的根元素加CSS变量,然后在第三方组件的内部样式里引用(不过如果第三方组件没暴露内部样式的引用位置,这招可能没用,但现在主流的组件库比如Element Plus、Ant Design Vue都暴露了大量的CSS变量)。
比如刚才的改`el-input`边框颜色的例子,用CSS变量的写法会更优雅:
```vue
<template>
<div class="my-form-container">
<!-- 给el-input的根元素加CSS变量 -->
<el-input v-model="inputVal" class="my-input" style="--el-input-border-color: red;" />
</div>
</template>
<!-- 或者写在scoped样式里,不需要深层穿透! -->
<style scoped>
.my-input {
--el-input-border-color: red;
}
</style>
哦对,这里要注意,CSS变量是可以继承的!所以你给第三方组件的根元素加的CSS变量,会自动继承到它的所有子元素,根本不需要深层穿透就能生效,而且性能比深层穿透好太多了,因为CSS变量的解析和渲染是浏览器原生支持的,非常快。
总结一下Vue3深层样式穿透的最佳实践
最后咱们再把所有知识点串一遍,整理成一份新手也能直接抄的最佳实践:
- 新项目100%用
deep(),不要用其他旧写法; - 维护Vue2老项目的话,先升级到Vue2.7,然后把所有旧写法换成
deep(); - 优先用第三方组件提供的CSS变量代替深层穿透;
- 只有在CSS变量解决不了问题的时候,才用
deep(),而且要尽量缩小作用范围、用具体的选择器; - 专门针对插槽的直接分发元素的样式,用
slotted(),尽量不要用deep()处理插槽元素的子元素,要么让使用方自己加,要么用CSS变量; - 千万不要滥用深层穿透,一个组件里尽量不要超过5-10个
deep()规则。
好了,今天关于Vue3 deep的所有内容就讲完了,如果还有什么不懂的地方,欢迎在评论区留言讨论哦!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


