【vue】单文件sfc里的scoped是什么?

679 阅读5分钟

1、样式污染

考虑这样的一个场景。有组件A组件B的style中定义了同样的类.h1, 那么打包之后的css文件中就会有两个同样的.h1类, 当页面展示组件A或者组件B的时候样式都同时受到了定义的两个.h1的影响,造成样式错乱。为了更好说明,我写了A和B两个组件,代码如下:

  1. 组件A
<template>
  <div class="compA">
    <h1 class="h1">A Hello</h1>
    <B></B>
  </div>
</template>

<script>
import B from "./B";
export default { 
  components: {
    B
  }
};
</script>

<style>
.h1 {
  margin: 40px 0 0;
  color: red;
}
</style>
  1. 组件B
<template>
  <div class="compB">
    <h1 class="h1">B</h1>
    <h1 class="h1">World</h1>
  </div>
</template>

<script>
export default {};
</script>

<style>
.h1 {
  margin: 40px 0 0;
  color: green;
}
</style>

打开页面,此时三个h1的颜色都是红色。

image.png

在组件B中.h1定义的color: green;被覆盖了。 打开浏览器控制台可以看到,下面的.h1将上面的.h1定义的color覆盖了。

image.png

2、使用scoped

  1. A组件
<template>
  <div class="compA">
    <h1 class="h1">A Hello</h1>
    <B></B>
  </div>
</template>

<script>
import B from "./B";
export default { 
  components: {
    B
  }
};
</script>

<style scoped>
.h1 {
  margin: 40px 0 0;
  color: red;
}
</style>
  1. B组件
<template>
  <div class="compB">
    <h1 class="h1">B</h1>
    <h1 class="h1">World</h1>
  </div>
</template>

<script>
export default {};
</script>

<style scoped>
.h1 {
  margin: 40px 0 0;
  color: green;
}
</style>

打开页面后,是这样显示的:

image.png

这才是我们想要的效果。打开控制台,发现有下面两个变化:

  1. 打包后的css选择器上多了data-v-XXXXXX image.png

  2. dom元素上多了data-v-XXXXXX

image.png

由此,我们大概能猜到style标签上加了scoped后编译后的css和页面中的dom元素都会加上data-v-XXXXXX。这样浏览器就能区分A组件的.h1和B组件的.h1

上面讲的是使用scoped来解决样式污染的问题。当然这并不是我的目的,就像标题说的那样,我希望尝试把scoped是什么以及是怎么样生效的讲清楚。从一下几个问题作为切入点。

  1. 什么是scopeId?scopeId是怎么生成的?
  2. 在sfc的template中我们并没有定义data-v这样的属性, 那为什么在页面的dom元素上会多出data-v-XXXXXX这个属性呢?
  3. 在sfc的style中我们写的选择器也没有加上data-v-XXXXXX,那这是怎么加上去的呢?

2、什么是scopeId? scopeId是怎么生成的?

每一个sfc组件都会有自己的唯一scopeId,类似data-v-XXXXXX中的XXXXXX就是scopeId。 我们都知道.vue单文件组件是需要经过vue-loader去处理的。既然每个vue文件的scopeId是唯一的,那么很容易联想到scopeId是不是vue-loader生成的。vue-loader里面的确有这样的逻辑。

  const id = hash(
    isProduction
      ? (shortFilePath + '\n' + source.replace(/\r\n/g, '\n'))
      : shortFilePath
  )

上面的代码就是生成scopeId执行的代码。

3、在sfc的template中我们并没有定义data-v这样的属性, 那为什么在页面的dom元素上会多出data-v-XXXXXX这个属性呢?

不知道大家有没有仔细看过组件导出的对象是什么样的?

import B from "./B";
console.log('组件B', B)

打印的结果:

image.png

导出的组件配置上会多一个_scopeId属性,这个属性不是我们自己定义上去的, 属性值data-v-5277df62是不是很熟悉?

这个值是不是和上面的dom截图中组件B内部dom元素上的scopeId一样的?

很明显sfc组件只是在编译阶段往组件配置对象上加了_scopeId属性, 属性值也不是随便写的, 是由vue-loader生成的。生成逻辑上面已经讲过了。

那么dom元素是什么时候加上scope的呢?在vue2patch逻辑中有这样一段代码,这段代码在src/core/vdom/patch.js中的createElm内。大概的逻辑就是在createElm内部创建真实的dom之后,调用setScope给dom元素添加scopeId

  vnode.elm = vnode.ns
    ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode)
  setScope(vnode)

setScope的逻辑是这样的:

  function setScope (vnode) {
    let i
    if (isDef(i = vnode.fnScopeId)) {
      nodeOps.setStyleScope(vnode.elm, i)
    } else {
      let ancestor = vnode
      while (ancestor) {
        if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
          nodeOps.setStyleScope(vnode.elm, i)
        }
        ancestor = ancestor.parent
      }
    }
    // for slot content they should also get the scopeId from the host instance.
    if (isDef(i = activeInstance) &&
      i !== vnode.context &&
      i !== vnode.fnContext &&
      isDef(i = i.$options._scopeId)
    ) {
      nodeOps.setStyleScope(vnode.elm, i)
    }
  }

dom元素上的data-v-XXXXXX是在运行时patch的过程中添加上去的。

4、在sfc的style中我们写的选择器也没有加上data-v-XXXXXX,那这是怎么加上去的呢?

这一切都是由一个叫stylePostLoader的loader来完成的。

  1. stylePostLoader处理css代码

image.png

  1. compileStyle内部使用postCss对css代码进行转换。下面的截图中可以看到使用了scoped时会使用一个scoped_1插件。

image.png

  1. 查看scoped_1插件的代码,发现有这样一段代码。
 selector.insertAfter(node, selectorParser.attribute({
    attribute: id
 }));

css代码上的data-v-XXXXXX是通过stylePostLoader加上去的。

5、/deep/的作用?

scopedId默认是加在选择器的最后一级上的。比如说下面的例子中有组件A组件B。在组件A中使用了组件B, 当在组件A中修改组件B的内部.el-autocomplete样式时:

.compA .el-autocomplete {
  width: 280px;
}

/* 编译后的代码 */
.compA .el-autocomplete[data-v-19388c91] {
  width: 280px;
}

使用/deep/

.compA /deep/.el-autocomplete {
  width: 280px;
}

/* 编译后的代码 */
.compA[data-v-19388c91] .el-autocomplete {
  width: 280px;
}

对比下面的dom结构图, 很明显知道了deep之后起作用的原因了。

210322-1536.png

  • 使用/deep/之前我们想要修改组件B内部带有.el-autocomplete的元素的样式,打包后使用的选择器是.compA .el-autocomplete[data-v-19388c91],很明显带有.el-autocomplete的元素上是不存在data-v-19388c91属性的,所以样式不起作用。
  • 使用/deep/之后呢?打包后的css是这样的.compA[data-v-19388c91] .el-autocomplete, 将scopedId移动到了.compA上, 此时样式是起作用的。

总结

  1. 当在父组件内给子组件的根节点修改样式时,其实是不用加/deep/的, 因为在子组件的根节点上同时有父组件和子组件的scopeId
  2. 当在父组件内给子组件的非根节点修改样式时需要带上/deep/,因为在父组件定义选择器的最后一级使用的是父组件的scopeId,而子组件中需要选择的元素却使用的是子组件的scopeId,所以是不会匹配上的,样式也不会生效。