对组件内使用:deep()修改自身样式和样式穿透的记录

44 阅读7分钟

一个简单的组件

<template>
    <div class="test">
        <div class="title" >
            我是标题t
        </div>
    </div>
</template>

此时正常生效颜色和字体大小

此时会编译为 .test .title[data-v-xxx]

.test{
    .title{
        font-size: 20px;
        color: red;
    }
}

但是如果加上:deep(),则不会生效,我们知道,此时按理说.title的[data-v-xxx]会被去掉,应该会被编译为

.test[data-v-xxxx] .title

.test{
    :deep(.title){
        font-size: 20px;
        color: red;
    }
}

但此时在浏览器style调试栏里没有任何对应的.title选择器

element.style {
}

用户代理样式表

div {
 display: block;
unicode-bidi: isolate;
}

如果去掉外围的.test选择器,则又会重新生效

    :deep(.title){
        font-size: 20px;
        color: red;
    }

此时在浏览器style调试页则会显示

   [data-v-xxx] .title {
 font-size: 20px;
 color: red;
}

表明当前其实选择器被编译为了[data-v-xxxx] .title


查询了AI得知疑似是因为vue内部对带有父元素选择器的 deep()进行了过滤,不参与编译结果,所以最终没有输出到浏览器style调试页上 但是在AI给出的来源文档里并没有看到对应的内容?疑似AI编造资料,特此记录

又重新问了一遍ai,ai又换说法了,把这种现象解释为vue处理css的时候将带有父元素的deep()处理为了.test [data-v-123] .title与原dom结构不同,所以不生效,在让ai给出参考的源文档中...


最后确认了,问题代码在经过vite编译后的结果显示

<template>
  <div class="test">
    <div class="title">测试标题</div>
  </div>
</template>

<style scoped>
.test {
  :deep(.title) {
    font-size: 20px;
    color: red;
  }
}
</style>

在最后输出的结果是.test [data-v-c64d93fd] .title,符合与DOM结构不同,所以没生效的原因

.test [data-v-c64d93fd] .title {
  font-size: 20px;
  color: red;
}

而之前的其他情况对应的是

:deep(.title) {
  font-size: 20px;
  color: red;
}

[data-v-16bfa729] .title {}符合原行为


添加一个祖先元素

  <div class="tabbar">
    <div class="test">
      <div class="title">测试标题</div>
    </div>
  </div>
  
.tabbar {
    .title {
  }
}

最后的结果是.tabbar [data-v-9ecd31d2] .title 由于祖先元素和title元素之间隔了一个带[data-v-xxx]的父元素,所以依旧能选中title


最后是最正确的写法

.test {
  .title {
    font-size: 20px;
    color: red;
  }
}

.test .title[data-v-bcdf58ac] {}

符合默认行为


如果是形如这种结构

<template>
  <div class="tabbar">
    <div class="test">
      <div class="title">测试标题</div>
    </div>
  </div>
</template>

<style scoped>
.tabbar {
  .test {
    :deep(.title) {
      font-size: 20px;
      color: red;
    }
  }
}
</style>

编译后结果为

.tabbar .test [data-v-81722b88] .title


由此终于能得到结论,如果闲的没事干,在自己的组件里使用了:deep()来样式穿透,vue会将原本的.title[data-v-bcdf58ac]转换成.[data-v-bcdf58ac] .title,如果.title前面有任何其他类,则会转换成.xxxx [data-v-bcdf58ac] .title,所以就会出现有时候能选中,有时候选不中


至于正常的使用,形如(子组件带scoped)

// 父组件
  <div class="tabbar">
    <div class="test">
      我是父组件
      <Cpn class="son" />
    </div>
  </div>
  
//子组件
    <div class="cpn">
        我是组件外层根元素
        <div class="cpn-inner">
            组件内部第二层
        </div>
    </div>

如果在父组件内直接对子组件的cpn-inner进行选中但不使用:deep()

.tabbar {
  .test {
    color: greenyellow;
    font-size: 20px;

    .cpn-inner {
      color: red;
      background-color: #666;
    }

  }
}

则最终的编译结果里,此时cpn的[data-v-xxx]是父组件的xxx,无法选中子组件

.tabbar .test .cpn-inner[data-v-110961b9] {
  color: red;
  background-color: #666;
}

在父组件内添加了:deep()之后

    :deep(.cpn-inner) {
      color: red;
      background-color: #666;
    }

最终的编译结果里,依旧是子组件的[data-v-xxx]被插入到之前的元素中,此时的[data-v-2eaeac9f]为外部父组件的[data-v],由于这个[data-v]其实也会被添加到子组件的根节点中,所以通过css子代选择器,就能被选中子组件内的模块

.tabbar .test [data-v-2eaeac9f] .cpn-inner {
  color: red;
  background-color: #666;
}

接下来再看看如果子组件不带scoped的情况

DOM结构则会变成引入第三方组件库的形式:仅有外层根节点会设置父组件的[data-v],内部模块不会添加[data-v]

    <div data-v-5725aaa2 class="tabbar">
    <div data-v-5725aaa2 class="test">
      我是父组件
      <div data-v-5725aaa2 class="cpn son">
        我是组件外层根元素
        <div class="cpn-inner">组件内部第二层</div>
      </div>
    </div>
  </div>
  
.tabbar {
  .test {
    color: greenyellow;
    font-size: 20px;

    :deep(.cpn-inner) {
      color: red;
      background-color: #666;
    }
  }
}

最后css的编译结果则是与之前相同的结构,[data-v-5725aaa2]为父组件[data-v]

.tabbar .test [data-v-5725aaa2] .cpn-inner {
  color: red;
  background-color: #666;
}

就能正确更改子组件模块内的样式了


最后的最后,假如我们的子组件里面还有嵌套,我们在父组件内要选择第三层嵌套的子元素cpn-inner2

// 子组件
<div class="cpn">
        我是组件外层根元素
        <div class="cpn-inner">
            组件内部第二层
            <div class="cpn-inner2">
                组件内部第三层
            </div>
        </div>
    </div>

// 父组件内
.tabbar {
  .test {
    color: greenyellow;
    font-size: 20px;

    .cpn-inner {
      color: red;
      background-color: #666;

      :deep(.cpn-inner2) {
        color: purple;
      }
    }
  }
}

输出此时css的编译结果


.tabbar .test[data-v-df3292a6] {
  color: #adff2f;
  font-size: 20px;
}
.tabbar .test .cpn-inner[data-v-df3292a6] {
  color: red;
  background-color: #666;
}
.tabbar .test .cpn-inner [data-v-df3292a6] .cpn-inner2 {
  color: purple;
}

输出的html结构如下

  <div data-v-df3292a6 class="tabbar">
    <div data-v-df3292a6 class="test">
      我是父组件
      <div data-v-df3292a6 class="cpn son">
        我是组件外层根元素
        <div class="cpn-inner">
          组件内部第二层
          <div class="cpn-inner2">组件内部第三层</div>
        </div>
      </div>
    </div>
  </div>

从中我们发现了重要的问题,[data-v-df3292a6]始终都是父组件的[data-v]

因此在选择.cpn-inner2的时候,inner2和inner之间并没有任何的[data-v]属性,所以仍然不会选中 对于上一层的.cpn-inner[data-v-df3292a6]来说,在DOM结构里cpn-inner并没有带任何的[data-v]属性,所以inner也不会被选中 这就导致了,在我们的:deep()写法里,无法选中任何的子组件内容


现在原因知道了,之后的操作就简单了

    :deep(.cpn-inner) {
      color: red;
      background-color: #666;

      .cpn-inner2 {
        color: purple;
      }
    }

选中子组件根节点下的第一个子元素,编译结果为

.test [data-v-df3292a6] .inner

.test [data-v-df3292a6] .cpn-inner .cpn-inner2

这时候两个子组件内的选中都能正常生效了


最后再做个总结:deep(.abc)的作用是:

如果当前为scoped环境,.abc有当前组件的[data-v]则将原本的.abc[data-v-xxx]转换为[data-v-xxx] .abc

如果.abc有父元素,则形式为.parent [data-v-xxx] .abc

如果.abc是子组件内的嵌套子元素,那就会在前面添加[data-v-父组件data-id] .abc

如果当前不是scoped环境,.abc没有[data-v],则会被转换为[data-v-父组件data-id] .abc,如果有父元素,则形式为.parent [data-v-父组件data-id] .abc

:deep()获取的[data-v]编号始终都是写:deep()时当前组件的[data-v-xxx]

知道这一点之后就明白该在哪里添加:deep()了


不过按照官方文档里的描述,在vue原始设计里:deep()是被设计成一个伪类

所以如果是按照伪类的正确写法,下面的代码编译后会变成

  .test {
    color: greenyellow;
    font-size: 20px;

    .son { // 子组件根节点
      font-size: 14px;

      &:deep(.cpn-inner2) {
        color: purple;
      }

    }
  }

.test .son[data-v-35d75636] .cpn-inner2 {}

此时父组件的[data-v]就正确的添加到了子组件的根节点上,之后一路写后面的子选择器即可,当然核心原理仍旧是:deep()对内部选择器的[data-v]的位置处理,搞懂了这点所有第三方组件的样式更改都能够通通拿下了