一、组件透传

1、简介

​ 组件透传又称组件继承,具体是指父子组件之间的attribute属性透传,所有在子组件标签上设置的attribute属性中,没有被子组件声明为propsemits的属性(包括v-on事件监听器),在渲染时都会被透传到子组件内的根元素上,最常见的就是classstyleid属性。

2、单根元素透传
属性透传:

​ 当子组件以单个元素作为根元素时,透传的attribute属性会被自动继承到该根元素上。以class为例,父组件给子组件标签设置class属性:

<!-- 在父组件中 给子组件标签上设置class属性 -->
<Child class="son" />

​ 子组件并没有将class声明为prop,子组件内的组件模板只有单个根元素:

<template>
  <!-- 单个根元素 -->
  <div>
    <h3>这是一个script setup子组件</h3>
  </div>
</template>

<script setup>
// 未设置props
</script>

​ 则最终子组件渲染完成后的DOM为:

<!-- 透传的属性自动继承到了根元素上 -->
<div class="son">
   <h3>这是一个script setup子组件</h3>
</div>
属性合并:

​ 当父组件向子组件内透传了classstyle属性,但子组件本身已经设置了classstyle属性时,两者的属性值会进行合并。合并规则如下:

  • 如果属性值之间无冲突,则两个属性值合并到根元素的对应属性上,根元素本身的属性值在前,父组件透传的属性在后。
  • 如果style的属性值之间存在冲突,针对同一样式设置了不同的样式效果,则最终样式效果以父组件的为准,不冲突的样式正常进行合并。
  • 如果class的属性值存在冲突,如果是类名相同,则两个相同的类名都会成为class的属性值;如果是类名不同,但针对同一样式设置了不同的样式效果,则样式效果以父组件的为准。因为父组件的类名在后,优先级更高。

父组件透传属性:

<Child class="son" style="border: 1px solid #ccc; color: #ccc" />

<style>
.son {
  background: #ccc;
}
</style>

子组件根元素设置属性:

<div class="son box" style="border: 1px solid red; font-size: 12px;"> 
 ...
</div>

<style>
.son {
  background: red;
}
.box {
  background: red;
}
</style>

最终子组件渲染完成后的DOM:

<div class="son box son" style="border: 1px solid #ccc;font-size: 12px;color: #ccc"> 
 ...
</div>

<style>
/* 最终有效的样式效果 */
.son {
  background: #ccc;
}
/* 其他样式依旧存在 只是优先级没有上面的高 */
</style>
属性冲突:

​ 当父组件向子组件内透传了其他属性值唯一属性,比如:titleid等,但子组件本身已经设置这类属性,两者属性值发生冲突,则最终属性值以父组件为准。

父组件透传属性:

<Child title="son-title" />

子组件根元素设置属性:

<div title="box-title" id="box"> 
 ...
</div>

最终子组件渲染完成后的DOM:

<div title="son-title" id="box"> 
 ...
</div>
事件透传:

​ 除了属性之外,同样的规则也适用于v-on绑定的事件监听器,如果在父组件中给子组件标签绑定事件监听器,该事件监听器会透传到子组件的根元素上,当子组件的根元素触发了相应事件之后,父组件中事件监听器绑定的事件处理函数会被触发。

​ 如果子组件的根元素本身也绑定事件监听器,则会先触发子组件的事件处理函数,再触发父组件中绑定的事件处理函数。

父组件透传事件处理器:

<Child @click="() => { console.log('父组件的事件监听器') }" />

子组件根元素绑定事件处理器:

<div @click="() => { console.log('子组件的事件监听器') }" >
  ...
</div>
深层透传:

​ 如果在接受透传的子组件中的根元素是另一个组件,也就是存在组件嵌套的情况,此时一级子组件接收的透传属性会继续透传给二级子组件。一级子组件中被peopsemits的接收的属性,不会再自动向下传递。

父组件透传属性:

<Child title="son-title" />

子组件内根元素为另一个组件:

<Child2 />

二级子组件内只有单个根元素:

<template>
  <!-- 单个根元素 -->
  <div>
    <h3>这是一个二级子组件</h3>
  </div>
</template>

最终的渲染的DOM:

<!-- 透传的属性自动添加到了根元素上 -->
<div title="son-title">
    <h3>这是一个二级子组件</h3>
</div>
3、禁用透传

​ 如果不想让子组件自动的继承父组件透传过来的属性,可以使用defineOptions()宏方法(v3.3新增)设置组件选项的inheritAttrs: false;,设置该属性后,组件的根元素就不会主动继承透传的属性:

<script setup>
defineOptions({
  // 禁止自动继承
  inheritAttrs: false
})
// ...setup 逻辑
</script>
在组件模板中访问透传属性:

​ 当子组件禁用透传之后,可以在子组件的组件模板中,通过$attrs来访问透传进来的属性,从而实现自定义透传属性作用的元素。 $attrs 中包含了除子组件中声明的 propsemits 之外的所有其他属性。

<p :title="$attrs.title">自定义透传属性的作用元素</p>

​ 与props不同,透传属性的属性名在$attrs中保留了原始的大小写和结构,例如:透传了一个名为data-test-1的属性,需要通过$attrs['data-test-1']的形式访问:

<p :data-test-1="$attrs['data-test-1']">自定义透传属性的作用元素</p>

​ 透传的事件监听器会成为$attrs上的一个函数,但函数名有所改变,变为:$attrs.on事件名格式,以@click为例,其会成为$attrs.onClick函数。想要将透传的事件监听器绑定到某个元素时,直接将目标元素的事件处理函数设置为$attrs中的对应函数即可:

父组件透传事件处理器:

<Child @click="countAdd()" @mouseenter="countAdd()" />

子组件绑定透传的事件处理器:

<p @click="$attrs.onClick" @mouseenter="$attrs.onMouseenter">此处继承事件处理器</p>

​ 还可以结合v-bind,将所有透传的属性绑定到一个元素上,因为没有参数的v-bind会将一个对象中的所有属性都绑定到目标元素上:

<p v-bind="$attrs"></p>
在JS中访问透传属性:

​ 当子组件禁用透传之后,可以在子组件的<script setup>中使用useAttrs()API来获取attrs对象,然后通过该对象访问所有透传进来的属性:

<script setup>
// 导入useAttrs()API
import { useAttrs } from 'vue'
// 禁止透传
defineOptions({
  inheritAttrs: false
})
// 获取attrs对象
const attrs = useAttrs();
// 访问所有透传的属性
console.log(attrs);
</script>

注意: 虽然获取的attrs对象会获取到最新透传的属性,但该对象并不是响应式的,无法通过侦听器来监听对象属性的变化。

4、多根元素透传

​ 有着多个根节点的子组件不会自动继承透传的属性,因为Vue 不知道该继承到哪个根元素上。需要在组件模板中,通过$attrs或者useAttrs()API显式的标明继承透传属性的元素,否则会抛出一个运行时警告。

子组件中有多个根节点:

<template>
<!-- 第一个根节点 -->
<div>
	<p @click="$attrs.onClick" :title="attrs.title">显式的指定继承透传属性的元素</p>
</div>
<!-- 第二个根节点 -->
<div>22222</div>
</template>

<script setup>
import { useAttrs } from 'vue'
  
// 获取attrs对象
const attrs = useAttrs();
</script>

二、组件插槽

1、简介

​ 组件插槽是一种将父组件中的模板片段插入到子组件模板中的功能,主要用于父组件向子组件中插入文本、HTML或其他组件等模板片段,而且插入子组件的模板片段可以访问到父组件的数据和样式,实现了更灵活的组件交互。

​ 组件插槽机制是以原生的Web Component 的<slot>元素 为原型创建的功能,感兴趣的可以了解一下。

2、基本用法

​ 首先需要在子组件内增加一个<slot>标签,表示模板片段要插入的位置:

<div class="content">
	 <!-- 模板片段将插入到这个位置 -->
   <slot></slot>
</div

​ 然后在父组件中使用子组件标签以双标签的形式包裹住要插入的模板片段:

<!-- 组件插槽 -->
<Child2>
    <!-- 要插入的模板片段 -->
    <p>这是父组件插入的模板片段</p>
</Child2>

​ 最终子组件的渲染的DOM结构为:

<div class="content">
	 <!-- 模板片段插入到指定位置 -->
   <p>这是父组件插入的模板片段</p>
</div

​ 其原理简单来说,就是将子组件标签包裹的那部分模板片段,替换到<slot>标签所在的位置上:

在这里插入图片描述

3、渲染作用域

​ 由于模板片段是写在父组件中的,所以模板片段可以访问到父组件中的数据,并受到父组件中样式的影响,子组件中只负责插槽外层样式的渲染,模板片段无法访问到子组件的数据。

​ 模板中的表达式只能访问其定义时所处的作用域,也就是说父组件模板中的表达式只能访问父组件的作用域,子组件模板中的表达式只能访问子组件的作用域。

父组件中设置模板样式,并在模板片段中使用响应式数据:

<template>
  <div>
    <!-- 子组件标签 -->
    <Child2>
      <p>这是父组件插入的模板片段</p>
      <!-- 定义类名 设置样式 -->
      <div class="box">
        <!-- 使用父组件中的数据 -->
        {{ test }}
      </div>
    </Child2>
  </div>
</template>

<script setup>
import Child2 from '../components/Child2.vue';
import { ref } from 'vue'

const test = ref('我是父组件中的变量');
</script>

<style>
.box {
  color: red;
}
</style>

子组件只需要指明插槽位置:

<div class="content">
	 <!-- 模板片段将插入到这个位置 -->
   <slot></slot>
</div

最终渲染结果为:

在这里插入图片描述

4、默认内容

​ 如果父组件没有传递模板片段,则子组件中slot插槽的位置将是空的。为了保证页面显示效果,我们可以给插槽设置一个默认内容,在未传递模板片段时进行显示。如果父组件传递了模板片段,则默认内容会被覆盖。

​ 具体操作为:在子组件内<slot>标签内部包裹想要显示的默认内容,包括样式和数据。由于此时默认内容是在子组件中定义的,因此可以访问到子组件的数据和样式。

<div class="content">
   <!-- 组件插槽 -->
   <slot>
     <!-- 默认内容 -->
     <p class="not-data">{{ type }}暂无数据~</p>
   </slot>
</div>
5、具名插槽

​ 如果一个子组件中具有多个组件插槽,每个插槽展示不同的模板片段,此时就需要用到具名插槽的概念。给组件中的每个插槽<slot>设置name属性,相当于一个唯一的ID,父组件传递组件模板时,根据name属性,指定要插入的插槽位置。设置了name属性的插槽被称为具名插槽,没有设置name属性的插槽,被称为默认插槽,其会隐式的设置namedefault

子组件中拥有多个插槽,并设置name属性:

<div class="content">
   <!-- 具有3个插槽 且插槽设置了name属性 -->
   <slot name="header"></slot>
   <slot name="body"></slot>
   <slot name="footer"></slot>
</div>

父组件中可以使用<template> +v-slot+插槽name实现指定插槽插入,其中v-slot也可以被简写为#

    <Child2>
      <!-- template包裹要传递的组件模板 v-slot指定要插入的插槽 -->
      <template v-slot:header>
        <p >这是父组件插入header的模板片段</p>
      </template>
       <!-- template包裹要传递的组件模板 # 指定要插入的插槽 -->
      <template #body>
        <p >这是父组件插入body的模板片段</p>
      </template>
       <!-- template包裹要传递的组件模板 # 指定要插入的插槽 -->
      <template #footer>
        <p >这是父组件插入footer的模板片段</p>
      </template>
    </Child2>

最终渲染结果为:

<div class="content">
  <!-- 按照插槽名称 一一对应插入 -->
	<p>这是父组件插入header的模板片段</p>
	<p>这是父组件插入body的模板片段</p>
	<p>这是父组件插入footer的模板片段</p>
</div>

具名插槽基本原理:

在这里插入图片描述

​ 当一个子组件即接收默认插槽又接收具名插槽时,父组件中所有位于子组件标签内顶级的非<template>节点都会被当做默认插槽的内容。

子组件:

<div class="content">
   <!-- 有设置了name属性的具名插槽 -->
   <slot name="header"></slot>
   <!-- 有没设置name的默认插槽 -->
   <slot></slot>
</div>

父组件:

    <Child2>
      <!-- 顶级的非<template>节点 会插入到默认插槽中 -->
      <div>这是父组件传递的模板片段</div>
      <!-- template包裹要传递的组件模板 v-slot指定要插入的插槽 -->
      <template v-slot:header>
        <p>这是父组件插入header的模板片段</p>
      </template>
      <!-- 顶级的非<template>节点 会插入到默认插槽中 -->
      <p>这是父组件传递的模板片段</p>
    </Child2>

最终渲染DOM结构:

<div class="content">
   <!-- 具名插槽插入的模板片段 -->
   <p>这是父组件插入header的模板片段</p>
   <!-- 默认插槽插入的模板片段 -->
   <div>这是父组件传递的模板片段</div>
   <p>这是父组件传递的模板片段</p>
</div>

注意: 一个组件插槽只能被插入一次,子组件的标签内不可以对一个插槽设置多个内容,但可以使用一个<template>包裹多个内容模块,然后再根据逻辑决定具体显示模块。但是子组件中可以设置多个同名插槽,当模板片段插入该插槽时,会同时插入到同名的所有插槽中:

子组件:

<div class="content">
  <!-- 同名插槽位置1 -->
	<slot name="header"></slot>
	<slot></slot>
  <!-- 同名插槽位置2 -->
	<slot name="header"></slot>
</div>

父组件:

<Child2>
  <!-- 顶级的非<template>节点 会插入到默认插槽中 -->
  <div>这是父组件传递的模板片段</div>
  <!-- template包裹要传递的组件模板 v-slot指定要插入的插槽 -->
  <template v-slot:header>
    <p>这是父组件插入header的模板片段</p>
  </template>
</Child2>

最终渲染的DOM结构:

<div class="content">
   <!-- 第一处header插槽插入的模板片段 -->
   <p>这是父组件插入header的模板片段</p>
   <!-- 默认插槽插入的模板片段 -->
   <div>这是父组件传递的模板片段</div>
   <!-- 第二处header插槽插入的模板片段  -->
   <p>这是父组件插入header的模板片段</p>
</div>
6、动态插槽名

​ 在使用v-slot(简写为#)绑定具名插槽时,可以将响应式变量作为要绑定插槽名,然后通过修改响应式变量的方式实现动态修改绑定的插槽:

<Child2>
  <!-- 绑定响应式变量作为插槽名 -->
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 # -->
  <template #[dynamicSlotName]>
    ...
  </template>
<Child2>
  
<script setup>
	import { ref } from 'vue';
  const dynamicSlotName = ref('header')
</script>
7、作用域插槽

​ 前面提到过,插入插槽的模板片段只能访问到父组件的数据,无法访问到子组件的数据。但可以通过给子组件的<slot>标签增加属性传递,然后在父组件的v-slot的属性值对象接收传递的属性,并在模板片段中使用,整体类似于props。这种可以将子组件作用域中的数据传递到父组件作用域中模板片段的插槽,称为作用域插槽。

默认作用域插槽:

子组件的<slot>标签上,以attributes属性的形式传递数据:

<div class="content">
	<slot text="这是子组件传递的数据"></slot>
</div>

父组件中子组件的标签上以v-slot="slotProps"的形式接收传递的数据:

<!-- 默认插槽接收 -->
<Child2 v-slot="slotProps">
	<p>这是父组件传递的模板片段</p>
	<p>{{ slotProps.text }}</p>
</Child2>

最终渲染DOM结构:

<div class="content">
	<p>这是父组件传递的模板片段</p>
	<p>这是子组件传递的数据</p>
</div>
具名作用域插槽:

​ 具名插槽想要传递数据的话,子组件<slot>上的数据传递方式不变,但是在父组件中需要以v-slot:name="slotProps"的形式获取传递的数据。

​ 如果具名插槽和默认插槽同时存在,则想要获取默认插槽传递的数据,需要使用其默认名称default

子组件:

<div class="content">
	<slot name="header" text="这是子组件具名插槽传递的数据" :count="1"></slot>
	<slot text="这是子组件默认插槽传递的数据"  :count="2"></slot>
</div>

父组件:

<Child2>
	<template #header="headerProps">
		<p>这是父组件插入header的模板片段</p>
		<p>{{ headerProps.text }}-{{ headerProps.count }}</p>
	</template>
	<template #default="defaultProps">
		<p>这是父组件插入默认插槽的模板片段</p>
		<p>{{ defaultProps.text }}-{{ defaultProps.count }}</p>
	</template>
</Child2>

最终渲染的DOM结构:

<div class="content">
	<p>这是父组件插入header的模板片段</p>
	<p>这是子组件具名插槽传递的数据-1</p>
  <p>这是父组件插入默认插槽的模板片段</p>
	<p>这是子组件默认插槽传递的数据-2</p>
</div>

注意:<slot>name属性不会作为属性传递到slotProps中。且每个插槽的slotProps只能在自己的模板片段中使用,不可在其他插槽的模板片段中使用。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐