插槽 Slot

文章目录

  • 前言
  • 一、为什么需要插槽
    • [1.1 问题](#1.1 问题)
    • [1.2 插槽的作用](#1.2 插槽的作用)
  • 二、默认插槽
    • [2.1 基本用法](#2.1 基本用法)
    • [2.2 默认内容](#2.2 默认内容)
  • 三、具名插槽
    • [3.1 定义](#3.1 定义)
    • [3.2 父组件使用](#3.2 父组件使用)
    • [3.3 简写规则](#3.3 简写规则)
  • 四、作用域插槽
    • [4.1 定义](#4.1 定义)
    • [4.2 父组件使用](#4.2 父组件使用)
    • [4.3 具名 + 作用域插槽](#4.3 具名 + 作用域插槽)
  • 五、应用场景
    • [5.1 通用卡片组件](#5.1 通用卡片组件)
    • [5.2 布局组件](#5.2 布局组件)
    • [5.3 表格自定义列](#5.3 表格自定义列)
    • [5.4 对话框组件](#5.4 对话框组件)
  • [六、Vue 2 vs Vue 3 语法](#六、Vue 2 vs Vue 3 语法)
    • [6.1 变化对照](#6.1 变化对照)
    • [6.2 注意事项](#6.2 注意事项)
  • 七、面试聚焦
    • [7.1 插槽内容的编译作用域](#7.1 插槽内容的编译作用域)
    • [7.2 作用域插槽 vs 普通插槽](#7.2 作用域插槽 vs 普通插槽)
    • [7.3 Vue 3 移除 slot-scope](#7.3 Vue 3 移除 slot-scope)
  • 八、易混淆点
  • 九、思考与练习
  • 总结

前言

插槽(Slot)是 Vue 的内容分发机制,让父组件可以向子组件模板中注入任意内容。本篇会讲清楚:

  • 默认插槽、具名插槽、作用域插槽
  • v-slot / # 简写
  • 插槽内容的编译作用域
  • Vue 2 到 Vue 3 的语法变化

一、为什么需要插槽

1.1 问题

vue 复制代码
<!-- 子组件 Card 只能写死内容 -->
<template>
  <div class="card">
    <h3>固定标题</h3>
    <p>固定内容</p>
  </div>
</template>

<!-- 父组件无法自定义卡片内部内容 -->
<Card />

1.2 插槽的作用

插槽允许父组件向子组件注入自定义内容,子组件只负责布局和容器,内容由父组件决定。

vue 复制代码
<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <slot>默认内容</slot>
  </div>
</template>

<!-- 父组件 -->
<Card>
  <h3>自定义标题</h3>
  <p>自定义内容</p>
</Card>

二、默认插槽

2.1 基本用法

vue 复制代码
<!-- 子组件 MyButton.vue -->
<template>
  <button class="btn">
    <slot>默认按钮文字</slot>
  </button>
</template>

<!-- 父组件 -->
<MyButton>提交</MyButton>
<!-- 渲染:<button class="btn">提交</button> -->

<MyButton />
<!-- 渲染:<button class="btn">默认按钮文字</button> -->

2.2 默认内容

<slot> 标签内的内容是后备内容,父组件未传入内容时显示:

vue 复制代码
<slot>
  <span>暂无内容</span>
</slot>

三、具名插槽

3.1 定义

具名插槽通过 name 属性区分多个插槽,父组件使用 v-slot:name#name 指定目标。

vue 复制代码
<!-- 子组件 Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>  <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

3.2 父组件使用

vue 复制代码
<template>
  <Layout>
    <!-- 具名插槽:v-slot:header 或 #header -->
    <template #header>
      <h1>页面标题</h1>
    </template>

    <!-- 默认插槽 -->
    <p>主要内容区域</p>

    <template #footer>
      <p>© 2026 版权所有</p>
    </template>
  </Layout>
</template>

3.3 简写规则

vue 复制代码
<!-- 完整写法 -->
<template v-slot:header>标题</template>

<!-- 简写(推荐) -->
<template #header>标题</template>

<!-- 默认插槽简写 -->
<template #default>内容</template>
<!-- 或直接写内容,省略 template -->
<Layout>内容</Layout>

四、作用域插槽

4.1 定义

作用域插槽将子组件的数据通过 props 传递给父组件,让父组件控制渲染逻辑。

vue 复制代码
<!-- 子组件 List.vue -->
<script setup>
defineProps({ items: Array })
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      <!-- 向父组件暴露 item 数据 -->
      <slot :item="item" :index="index">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

4.2 父组件使用

vue 复制代码
<template>
  <!-- 接收子组件传递的数据 -->
  <List :items="list" v-slot="{ item }">
    <b>{{ item.name }}</b> - {{ item.desc }}
  </List>

  <!-- 解构多个 prop -->
  <List :items="list" v-slot="{ item, index }">
    {{ index + 1 }}. {{ item.name }}
  </List>
</template>

4.3 具名 + 作用域插槽

vue 复制代码
<!-- 子组件 Table.vue -->
<script setup>
defineProps({
  data: { type: Array, required: true },
  columns: { type: Array, required: true }
})
</script>

<template>
  <table>
    <thead>
      <tr>
        <!-- 向 #header 插槽暴露 columns -->
        <slot name="header" :columns="columns"></slot>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <!-- 向 #row 插槽暴露当前行数据 -->
        <slot name="row" :row="row"></slot>
      </tr>
    </tbody>
  </table>
</template>
vue 复制代码
<!-- 父组件 -->
<script setup>
const users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
]
const columns = ['姓名', '邮箱']
</script>

<template>
  <Table :data="users" :columns="columns">
    <template #header="{ columns }">
      <th v-for="col in columns" :key="col">{{ col }}</th>
    </template>
    <template #row="{ row }">
      <td>{{ row.name }}</td>
      <td>{{ row.email }}</td>
    </template>
  </Table>
</template>

说明:

  • 子组件通过 defineProps 接收 datacolumns
  • #header 插槽接收子组件传出的 columns,渲染表头
  • #row 插槽在每一行循环中接收 row,渲染单元格

五、应用场景

5.1 通用卡片组件

vue 复制代码
<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">默认标题</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
  </div>
</template>

<!-- 使用 -->
<Card>
  <template #header>用户信息</template>
  <p>姓名:Alice</p>
</Card>

5.2 布局组件

vue 复制代码
<!-- PageLayout.vue -->
<template>
  <div class="page">
    <aside><slot name="sidebar" /></aside>
    <main><slot /></main>
  </div>
</template>

5.3 表格自定义列

vue 复制代码
<!-- DataTable.vue -->
<template>
  <table>
    <tr v-for="row in rows" :key="row.id">
      <td v-for="col in columns" :key="col.key">
        <slot :name="col.key" :row="row" :value="row[col.key]">
          {{ row[col.key] }}
        </slot>
      </td>
    </tr>
  </table>
</template>

<!-- 父组件自定义某列渲染 -->
<DataTable :rows="data" :columns="cols">
  <template #status="{ value }">
    <span :class="value">{{ value }}</span>
  </template>
</DataTable>

5.4 对话框组件

vue 复制代码
<!-- Dialog.vue -->
<template>
  <div class="dialog">
    <div class="dialog-header">
      <slot name="title">提示</slot>
    </div>
    <div class="dialog-body">
      <slot />
    </div>
    <div class="dialog-footer">
      <slot name="footer">
        <button @click="$emit('close')">关闭</button>
      </slot>
    </div>
  </div>
</template>

六、Vue 2 vs Vue 3 语法

6.1 变化对照

Vue 2 Vue 3
slot="header" #headerv-slot:header
slot-scope="{ item }" #default="{ item }"
具名 + 作用域分开写 统一用 v-slot
vue 复制代码
<!-- Vue 2 -->
<List :items="list">
  <template slot="item" slot-scope="{ item }">
    {{ item.name }}
  </template>
</List>

<!-- Vue 3 -->
<List :items="list">
  <template #item="{ item }">
    {{ item.name }}
  </template>
</List>

6.2 注意事项

vue 复制代码
<!-- ❌ v-slot 不能直接用在普通元素上 -->
<div #header>标题</div>

<!-- ✅ 必须用在 template 上 -->
<template #header>标题</template>

<!-- ✅ 默认插槽可以省略 template -->
<MyComponent>直接写内容</MyComponent>

七、面试聚焦

7.1 插槽内容的编译作用域

vue 复制代码
<!-- 插槽内容在父组件作用域中编译 -->
<script setup>
const parentMsg = '来自父组件'
</script>

<template>
  <Child>
    {{ parentMsg }}  <!-- ✅ 可以访问父组件数据 -->
    <!-- {{ childMsg }}  ❌ 无法访问子组件数据(除非作用域插槽) -->
  </Child>
</template>

7.2 作用域插槽 vs 普通插槽

javascript 复制代码
// 普通插槽:父组件向子组件注入内容
// 作用域插槽:子组件向父组件暴露数据,父组件决定如何渲染

// 典型场景:表格/List 组件暴露 row 数据,父组件自定义列渲染

7.3 Vue 3 移除 slot-scope

javascript 复制代码
// Vue 2: slot + slot-scope 两个属性
// Vue 3: 统一 v-slot(简写 #),语法更一致

八、易混淆点

  1. 插槽内容在父组件作用域编译:只能访问父组件的数据和方法,不能直接访问子组件内部数据。
  2. 作用域插槽的数据是只读的:父组件不能通过插槽 props 修改子组件内部状态。
  3. v-slot 只能用在 <template>:默认插槽内容可以直接写,具名/作用域插槽需用 template。
  4. Vue 3 移除 slot / slot-scope :统一使用 v-slot# 简写。
  5. 默认插槽名是 default<slot> 等价于 <slot name="default">

九、思考与练习

1. 插槽和普通 Props 传递 HTML 有什么区别?

解析:

  • Props:传递数据,子组件控制如何渲染
  • 插槽:传递内容/结构,父组件控制渲染什么

2. 作用域插槽和普通插槽的区别?

解析:

  • 普通插槽:父 → 子,父组件注入内容
  • 作用域插槽:子 → 父,子组件暴露数据,父组件决定渲染方式

3. 插槽内容在哪个作用域编译?

解析:在父组件作用域编译,只能访问父组件的数据和方法。需要子组件数据时使用作用域插槽。

4. Vue 3 中如何写具名作用域插槽?

vue 复制代码
<Child v-slot:item="{ row }">
  {{ row.name }}
</Child>
<!-- 或 -->
<Child #item="{ row }">
  {{ row.name }}
</Child>

5. 为什么 v-slot 不能直接用在 div 等元素上?

解析:v-slot 是编译时指令,用于标记插槽内容边界。Vue 3 要求它用在 <template> 上,以便编译器正确识别插槽边界;默认插槽的内容可以直接写在组件标签内。


总结

  • 插槽:Vue 的内容分发机制,父组件向子组件注入内容
  • 默认插槽<slot>,父组件直接写内容
  • 具名插槽<slot name="xxx">,父组件用 #xxx 指定
  • 作用域插槽 :子组件通过 <slot :prop="val"> 暴露数据,父组件自定义渲染
  • 编译作用域:插槽内容在父组件作用域编译
  • Vue 3 :统一 v-slot / #,移除 slot-scope