讲解

props 传数据,插槽传「内容」。一个卡片组件,标题、正文、底部按钮各不相同,不该做成十几个 props,而该留几个「空洞」让使用方填——这就是插槽。默认插槽对应组件标签里的内容:<Card>这里是内容</Card>,Card 内部用 <slot></slot> 承接。具名插槽解决「多个洞」:<slot name="header"> 配合 <template #header>,一个组件可以有头部、正文、底部多个分发口。

插槽内容的一个关键规则是作用域:插槽内容在父组件作用域编译,访问的是父组件的数据——<Card>{{ userName }}</Card> 里的 userName 是父组件的,跟 Card 内部的状态无关。这符合直觉:内容是父组件写的。

但反过来怎么办?子组件循环渲染列表,每一项的展示想交给父组件定制,父组件又需要拿到「当前这一项」的数据——这就是作用域插槽:子组件在 <slot :item="row"> 上把数据抛出来,父组件用 <template #default="{ item }"> 解构接收。数据从子流向父,渲染权从父回到父,表格、列表、下拉选项组件全靠这个模式撑起来。配合 v-if 还可以给插槽写兜底内容:<slot>默认文案</slot>,使用方不传时显示。

示例

用「函数传参」类比作用域插槽:插槽本质上就是子组件回调父组件传下来的渲染函数(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// 子组件:渲染列表,每一项的 HTML 由「插槽函数」决定
function List({ items }, slots) {
  return items
    .map((item) => {
      const render = slots.item || (({ item }) => item.text); // 兜底内容
      return '<li>' + render({ item, index: items.indexOf(item) }) + '</li>';
    })
    .join('');
}

// 场景一:父组件不提供插槽 → 用兜底
const plain = List({ items: [{ text: '苹果' }, { text: '香蕉' }] }, {});
assert.strictEqual(plain, '<li>苹果</li><li>香蕉</li>');

// 场景二:父组件提供作用域插槽 → 完全自定义
const fancy = List(
  { items: [{ text: '苹果' }, { text: '香蕉' }] },
  {
    item: ({ item, index }) => '<b>' + (index + 1) + '. ' + item.text + '</b>',
  },
);
assert.strictEqual(fancy, '<li><b>1. 苹果</b></li><li><b>2. 香蕉</b></li>');

console.log('兜底渲染:', plain);
console.log('作用域插槽:', fancy);
console.log('数据在子组件,渲染权在父组件——这就是作用域插槽');

真实组件:

<script setup>
// DataList.vue
defineProps({ items: { type: Array, required: true } });
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      <slot name="item" :item="item" :index="index">
        {{ item.text }}
      </slot>
    </li>
  </ul>
</template>
<script setup>
// Parent.vue
import { ref } from 'vue';
import DataList from './DataList.vue';

const fruits = ref([{ text: '苹果' }, { text: '香蕉' }]);
</script>

<template>
  <DataList :items="fruits">
    <template #item="{ item, index }">
      <b>{{ index + 1 }}. {{ item.text }}</b>
    </template>
  </DataList>
</template>

常见坑

  • 搞混插槽的作用域:插槽里直接访问子组件内部变量是拿不到的,要子组件通过 slot props 显式抛出来。
  • 具名插槽名字不匹配<slot name="header"> 对应 <template #header>(或 v-slot:header),名字差一个字符就静默走兜底。
  • 在循环外解构 slot props:作用域插槽的数据每一项都不同,template 必须放在 v-for 对应的层级。
  • 把复杂逻辑塞进插槽内容:插槽内容也是模板,复杂推导先 computed 再填进来。
  • 过度设计插槽:只有一两个定制点时 props 更简单,插槽留给「整块内容都要定制」的场景。

小结

插槽分发内容、作用域归父组件;作用域插槽让子组件把数据抛给父组件渲染——列表/表格组件的核心模式。下一章看组件从出生到销毁的全过程:生命周期。