SSF0SSF0
首页
前端
  • Node
  • Go
  • C#
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • C#
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的`this`指向问题详解
    • SessionStorage踩坑记录:它真的能"只设置一次"吗?
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
  • es6

    • Promise
    • es6 模块导出方式全解析
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue 3 深度选择器:deep()完全指南
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • 使用 pnpm 构建 Monorepo 实战指南
    • pnpm 修改依赖源码打包报错
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧

Vue 3 深度选择器:deep()完全指南

一、Scoped 样式机制工作原理深度解析

在 Vue 中,<style scoped>通过 PostCSS 将组件样式转换为带有唯一属性选择器的样式,确保样式隔离。

编译流程详解

1. 标识符生成

每个组件被分配唯一的data-v-xxx标识符。

2. 元素属性注入

  • 根元素: 同时接收自身组件的data-v和父组件的data-v属性

  • 内部元素: 只接收自身组件的data-v属性

3. 选择器转换

<!-- 组件代码 -->
<template>
  <div class="example">Hello</div>
</template>

<style scoped>
.example {
  color: red;
}
</style>

<!-- 编译转换过程 -->
选择器: .example → .example[data-v-f3f3eg9] 元素:
<div class="example"></div>

二、为什么根元素可直接设置而内部元素需要:deep()?

这是 Vue 3 scoped 样式最核心的机制,通过一个真实案例详细解释:

<template>
  <v-navigation-drawer :width="isCollapsed ? 80 : 250">
    <div class="p-4 h-100 d-flex flex-column">
      <HeadLogo />
      <ActionList />
      <InfoAndSet />
    </div>
  </v-navigation-drawer>
</template>

<style scoped>
.v-navigation-drawer {
  background: none !important; /* 直接生效 */
}
</style>

编译后的实际效果

<!-- 渲染后HTML (简化表示) -->
<nav class="v-navigation-drawer" data-v-sidebar>
  <div class="p-4 h-100 d-flex flex-column" data-v-sidebar>
    <div class="head-logo" data-v-headlogo data-v-sidebar>...</div>
    <div class="action-list" data-v-actionlist data-v-sidebar>...</div>
    <div class="info-set" data-v-infoset data-v-sidebar>...</div>
  </div>
</nav>

<!-- 编译后CSS -->
<style>
  .v-navigation-drawer[data-v-sidebar] {
    background: none !important;
  }
</style>

子组件内部元素样式设置

如果要设置子组件内部元素的样式:

<style scoped>
/* 不会生效 - 编译为 .head-logo .title[data-v-sidebar] */
.head-logo .title {
  color: red;
}

/* 会生效 - 编译为 .head-logo .title[data-v-headlogo] */
:deep(.head-logo .title) {
  color: red;
}
</style>

三、:deep()选择器详解

1. 语法演变

:deep()替代了 Vue 2 中的>>>、/deep/和::v-deep:

/* Vue 2语法 */
.parent >>> .child {
  /* 样式 */
}

/* Vue 3语法 */
:deep(.child) {
  /* 样式 */
}

2. 不同使用场景示例

基本用法:

<style scoped>
:deep(.third-party-button) {
  background-color: #42b983;
}
</style>

选择器组合与多层嵌套:

<style scoped>
/* 组合选择器 */
.my-component :deep(.child-element) {
  color: blue;
}

/* 多层嵌套 */
:deep(.parent .child .grandchild) {
  font-weight: bold;
}
</style>

四、深入理解 Vue 3 的编译行为

1. 编译器如何处理不同元素

考虑以下组件嵌套:

<!-- ParentComponent.vue (data-v-parent) -->
<template>
  <div class="parent">
    <ChildComponent class="child" />
  </div>
</template>

<!-- ChildComponent.vue (data-v-child) -->
<template>
  <div class="child-root">
    <p class="content">内容</p>
  </div>
</template>

编译后的 HTML 结构:

<div class="parent" data-v-parent>
  <!-- 子组件的根元素同时拥有两个data-v属性 -->
  <div class="child-root" data-v-child data-v-parent>
    <!-- 子组件内部元素只有自己的data-v属性 -->
    <p class="content" data-v-child>内容</p>
  </div>
</div>

2. 不同选择器的编译结果

<style scoped>
/* 父组件中的选择器 */
.child {
  border: 1px solid red;
}
/* 编译为: .child[data-v-parent] { border: 1px solid red; } */

.child .content {
  color: blue;
}
/* 编译为: .child[data-v-parent] .content[data-v-parent] { color: blue; } */
/* 不生效,因为内部元素没有data-v-parent属性 */

:deep(.child .content) {
  color: blue;
}
/* 编译为: .child .content[data-v-child] { color: blue; } */
/* 生效,因为使用了:deep()选择器 */
</style>

3. 多层嵌套中的 data-v 属性传递机制

在 Vue 3 中,data-v-xxx标识符的传递是非递归的,只会影响直接子组件的根元素。这一点在多层嵌套的组件结构中尤为重要:

<!-- 组件嵌套层次: GrandParent > Parent > Child > GrandChild -->

<!-- GrandParentComponent.vue (data-v-grandparent) -->
<template>
  <div class="grand-parent">
    <ParentComponent />
  </div>
</template>

<!-- 多层嵌套后的实际渲染结果 -->
<div class="grand-parent" data-v-grandparent>
  <div class="parent" data-v-parent data-v-grandparent>
    <div class="child" data-v-child data-v-parent>
      <div class="grand-child" data-v-grandchild data-v-child>
        <p class="text" data-v-grandchild>内容</p>
      </div>
    </div>
  </div>
</div>

关键规则:

  1. 每个组件的根元素只接收自身的data-v和直接父组件的data-v,不会传递祖先组件的data-v

  2. data-v属性的传递是单层的,不会递归向下传递

  3. 因此在 GrandParent 组件中,要影响 GrandChild 内部元素,需要多重:deep()选择器

<!-- GrandParentComponent.vue -->
<style scoped>
/* 只影响Parent组件根元素 - 可以直接设置 */
.parent {
  border: 1px solid red;
}

/* 影响Child组件根元素 - 需要一个:deep() */
:deep(.child) {
  padding: 10px;
}

/* 影响GrandChild组件根元素 - 需要嵌套:deep() */
:deep(.child :deep(.grand-child)) {
  margin: 5px;
}

/* 影响GrandChild内部文本元素 - 多层:deep()嵌套 */
:deep(.parent :deep(.child :deep(.text))) {
  color: blue;
}

/* 实际开发中推荐的替代写法 - 更清晰 */
:deep(.child) :deep(.grand-child) :deep(.text) {
  color: blue;
}
</style>

这种机制确保了样式隔离的严格性,避免了远祖先组件意外影响深层嵌套组件的样式,同时通过显式的:deep()声明提高了代码的可维护性。

五、实际应用场景示例

1. 修改 Vuetify 组件样式

<template>
  <v-card>
    <v-card-title>标题</v-card-title>
    <v-card-text>内容</v-card-text>
  </v-card>
</template>

<style scoped>
/* 根元素直接设置 */
.v-card {
  border-radius: 8px;
}

/* 内部元素需要:deep() */
:deep(.v-card-title) {
  font-size: 18px;
}
</style>

2. 插槽内容样式处理

<template>
  <div class="wrapper">
    <slot></slot>
  </div>
</template>

<style scoped>
/* 插槽内容需要:deep() */
:deep(.slot-content) {
  margin: 10px;
}
</style>

3. 从 index.vue 实例看样式渗透机制

<!-- sideNavbar/index.vue -->
<style scoped>
/* 可以生效 - 根元素 */
.v-navigation-drawer {
  background: none !important;
}

/* 可以生效 - 直接子元素 */
.p-4 {
  padding: 20px !important;
}

/* 不会生效 - 子组件内部元素 */
.head-logo .avatar {
  border: 2px solid red;
}

/* 会生效 - 使用:deep()穿透 */
:deep(.head-logo .avatar) {
  border: 2px solid red;
}
</style>

六、深度选择器的调试技巧

1. 检查 DOM 元素属性

开发时,可以通过浏览器开发者工具检查元素的data-v-xxx属性,理解样式作用域。

2. 识别编译后的选择器

在浏览器开发者工具中查看编译后的 CSS,可以看到所有选择器都转换为[data-v-xxx]形式。

3. 排查常见问题

  • 样式不生效:检查是否需要使用:deep()

  • 选择器过于复杂:简化选择器路径

  • 全局污染:避免无作用域样式的滥用

七、最佳实践指南

1. 合理使用组件 API

尽量通过 props 和 slots 控制样式,而非强行覆盖内部样式。

2. 利用根元素特性

<template>
  <div class="wrapper">
    <third-party-component />
  </div>
</template>

<style scoped>
/* 包装一层可避免使用:deep() */
.wrapper .specific-class {
  margin-top: 20px;
}
</style>

3. 样式分层原则

<style scoped>
/* 自身布局样式 */
.component {
  display: grid;
}

/* 必要的样式渗透 */
:deep(.lib-component) {
  margin: 10px;
}
</style>

八、深度选择器机制的通俗理解

根元素与内部元素样式渗透机制解释

子组件的根元素在渲染时会同时接收两个属性:

  1. 自身组件的data-v-xxx属性

  2. 父组件的data-v-xxx属性

因此当你在父组件中编写选择器时:

  • .some-class → .some-class[data-v-parent](父组件的标识)

  • 子组件根元素拥有data-v-parent属性,所以样式生效

  • 无需使用:deep()

而子组件内部元素只接收一个属性:

  • 只有子组件自己的data-v-child属性

  • 没有父组件的data-v-parent属性

  • 所以父组件的.some-inner-class样式无法匹配

  • 必须使用:deep()来调整选择器的编译结果

这是 Vue 3 设计的巧妙之处:允许父组件样式自然地影响子组件的"外表"(根元素),同时保护子组件的"内部"(内部元素)不受干扰,除非明确使用:deep()声明穿透。

九、总结

Vue 3 的 scoped 样式机制通过巧妙的编译转换实现了样式隔离与可控渗透:

  1. 根元素双重标记机制:根元素同时拥有父组件和自身的data-v属性,使得父组件样式可直接作用于子组件的根元素。

  2. 选择性渗透::deep()选择器允许你明确声明哪些样式需要渗透到子组件内部。

  3. 自动编译转换:所有选择器都会被自动转换为带有组件唯一标识符的属性选择器。

  4. 单层传递原则:data-v属性只向下传递一层,不会递归传递,确保样式隔离的严格性。

通过理解这些原理,你可以更精确地控制组件样式的作用范围,既保持组件样式的隔离性,又能在必要时突破边界实现样式定制。

最后更新时间:
贡献者: 何风顺
上一页
在Vue 3项目中顺利集成Tailwind CSS的完整指南