阅读视图

发现新文章,点击刷新页面。

Vue插槽

一、先明确核心概念

  1. 具名插槽:给 <slot> 标签添加 name 属性,用于区分不同位置的插槽,让父组件可以精准地将内容插入到子组件的指定位置,解决「默认插槽只能插入一处内容」的问题。
  2. 默认插槽:没有 name 属性的 <slot>,是具名插槽的特殊形式(默认名称为 default),父组件中未指定插槽名称的内容,会默认插入到这里。
  3. 插槽默认内容:在子组件的 <slot> 标签内部写入内容,当父组件未给该插槽传递任何内容时,会显示这份默认内容;若父组件传递了内容,会覆盖默认内容,提升组件的复用性和容错性。
  4. 作用域插槽:子组件通过「属性绑定」的方式给 <slot> 传递内部私有数据,父组件在使用插槽时可以接收这些数据并自定义渲染,解决「父组件无法访问子组件内部数据」的问题,实现「子组件供数、父组件定制渲染」。

二、分步实例演示

第一步:实现最基础的「具名插槽 + 默认插槽」

核心需求:创建一个通用的「页面容器组件」,包含「页头」「页面内容」「页脚」三个部分,其中「页面内容」用默认插槽,「页头」「页脚」用具名插槽。

1. 子组件:定义插槽(文件名:PageContainer.vue

<template>
  <!-- 通用页面容器样式(简单美化,方便查看效果) -->
  <div class="page-container" style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 20px 0;">
    <!-- 具名插槽:页头(name="header") -->
    <div class="page-header" style="border-bottom: 1px dashed #e0e0e0; padding-bottom: 10px; margin-bottom: 10px;">
      <slot name="header" />
    </div>

    <!-- 默认插槽:页面核心内容(无name属性,对应default) -->
    <div class="page-content" style="margin: 20px 0; min-height: 100px;">
      <slot />
    </div>

    <!-- 具名插槽:页脚(name="footer") -->
    <div class="page-footer" style="border-top: 1px dashed #e0e0e0; padding-top: 10px; margin-top: 10px; text-align: right;">
      <slot name="footer" />
    </div>
  </div>
</template>

<script setup>
// 子组件无需额外逻辑,仅定义插槽结构即可
</script>

2. 父组件:使用插槽(传递内容,文件名:App.vue

父组件通过 v-slot:插槽名(简写:#插槽名)指定内容要插入的具名插槽,未指定的内容默认插入到默认插槽。

<template>
  <h2>基础具名插槽 + 默认插槽演示</h2>

  <!-- 使用子组件 PageContainer -->
  <PageContainer>
    <!-- 给具名插槽 header 传递内容(简写 #header,完整写法 v-slot:header) -->
    <template #header>
      <h3>这是文章详情页的页头</h3>
      <nav>首页 > 文章 > Vue 插槽教程</nav>
    </template>

    <!-- 未指定插槽名,默认插入到子组件的默认插槽 -->
    <div>
      <p>1. 具名插槽可以让父组件精准控制内容插入位置。</p>
      <p>2. 默认插槽用于承载组件的核心内容,使用更简洁。</p>
      <p>3. 这部分内容会显示在页头和页脚之间。</p>
    </div>

    <!-- 给具名插槽 footer 传递内容(简写 #footer) -->
    <template #footer>
      <span>发布时间:2026-01-13</span>
      <button style="margin-left: 20px; padding: 4px 12px;">收藏文章</button>
    </template>
  </PageContainer>
</template>

<script setup>
// 导入子组件
import PageContainer from './PageContainer.vue';
</script>

3. 运行效果与说明

  • 页头区域显示「文章详情页标题 + 面包屑导航」(对应 #header 插槽内容)。
  • 中间区域显示核心正文(对应默认插槽内容)。
  • 页脚区域显示「发布时间 + 收藏按钮」(对应 #footer 插槽内容)。
  • 关键:父组件的 <template> 标签包裹插槽内容,通过 #插槽名 绑定子组件的具名插槽,结构清晰,互不干扰。

第二步:实现「带默认内容的插槽」

核心需求:优化上面的 PageContainer.vue,给「页脚插槽」添加默认内容(默认显示「返回顶部」按钮),当父组件未给 footer 插槽传递内容时,显示默认按钮;若传递了内容,覆盖默认内容。

1. 修改子组件:给插槽添加默认内容(PageContainer.vue

仅修改 footer 插槽部分,在 <slot name="footer"> 内部写入默认内容:

<template>
  <div class="page-container" style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 20px 0;">
    <!-- 具名插槽:页头 -->
    <div class="page-header" style="border-bottom: 1px dashed #e0e0e0; padding-bottom: 10px; margin-bottom: 10px;">
      <slot name="header" />
    </div>

    <!-- 默认插槽:页面核心内容 -->
    <div class="page-content" style="margin: 20px 0; min-height: 100px;">
      <slot />
    </div>

    <!-- 具名插槽:页脚(带默认内容) -->
    <div class="page-footer" style="border-top: 1px dashed #e0e0e0; padding-top: 10px; margin-top: 10px; text-align: right;">
      <slot name="footer">
        <!-- 插槽默认内容:父组件未传递footer内容时,显示该按钮 -->
        <button style="padding: 4px 12px;" @click="backToTop">返回顶部</button>
      </slot>
    </div>
  </div>
</template>

<script setup>
// 定义默认内容的点击事件(返回顶部)
const backToTop = () => {
  window.scrollTo({
    top: 0,
    behavior: 'smooth' // 平滑滚动
  });
};
</script>

2. 父组件演示两种场景(App.vue

分别演示「不传递 footer 内容」和「传递 footer 内容」的效果:

<template>
  <h2>带默认内容的插槽演示</h2>

  <!-- 场景1:父组件不传递 footer 插槽内容,显示子组件的默认「返回顶部」按钮 -->
  <h4>场景1:未传递页脚内容(显示默认按钮)</h4>
  <PageContainer>
    <template #header>
      <h3>这是未传递页脚的页面</h3>
    </template>
    <p>该页面父组件没有给 footer 插槽传递内容,所以页脚会显示子组件默认的「返回顶部」按钮。</p>
  </PageContainer>

  <!-- 场景2:父组件传递 footer 插槽内容,覆盖默认按钮 -->
  <h4 style="margin-top: 40px;">场景2:传递页脚内容(覆盖默认按钮)</h4>
  <PageContainer>
    <template #header>
      <h3>这是传递了页脚的页面</h3>
    </template>
    <p>该页面父组件给 footer 插槽传递了自定义内容,会覆盖子组件的默认「返回顶部」按钮。</p>
    <template #footer>
      <span>作者:Vue 小白教程</span>
      <button style="margin-left: 20px; padding: 4px 12px;">点赞</button>
      <button style="margin-left: 10px; padding: 4px 12px;">评论</button>
    </template>
  </PageContainer>
</template>

<script setup>
import PageContainer from './PageContainer.vue';
</script>

3. 运行效果与说明

  • 场景1:页脚显示「返回顶部」按钮,点击可实现平滑滚动到页面顶部(默认内容生效)。
  • 场景2:页脚显示「作者 + 点赞 + 评论」,默认的「返回顶部」按钮被覆盖(自定义内容生效)。
  • 核心价值:插槽默认内容让组件更「健壮」,无需父组件每次都传递所有插槽内容,减少冗余代码,提升组件复用性。

第三步:实际业务场景综合应用(卡片组件)

核心需求:创建一个通用的「商品卡片组件」,使用具名插槽实现「商品图片」「商品标题」「商品价格」「操作按钮」的自定义配置,其中「操作按钮」插槽带默认内容(默认「加入购物车」按钮)。

1. 子组件:商品卡片(GoodsCard.vue

<template>
  <div class="goods-card" style="width: 280px; border: 1px solid #f0f0f0; border-radius: 12px; padding: 16px; margin: 16px; float: left; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
    <!-- 具名插槽:商品图片 -->
    <div class="goods-img" style="width: 100%; height: 180px; margin-bottom: 12px; text-align: center;">
      <slot name="image" />
    </div>

    <!-- 具名插槽:商品标题 -->
    <div class="goods-title" style="font-size: 16px; font-weight: 500; margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
      <slot name="title" />
    </div>

    <!-- 具名插槽:商品价格 -->
    <div class="goods-price" style="font-size: 18px; color: #ff4400; margin-bottom: 16px;">
      <slot name="price" />
    </div>

    <!-- 具名插槽:操作按钮(带默认内容) -->
    <div class="goods-actions" style="text-align: center;">
      <slot name="action">
        <!-- 默认内容:加入购物车按钮 -->
        <button style="width: 100%; padding: 8px 0; background: #ff4400; color: #fff; border: none; border-radius: 8px; cursor: pointer;">
          加入购物车
        </button>
      </slot>
    </div>
  </div>
</template>

<script setup>
// 无需额外逻辑,仅提供插槽结构和默认内容
</script>

2. 父组件:使用商品卡片组件(App.vue

自定义不同商品的内容,演示插槽的灵活性:

<template>
  <h2>实际业务场景:商品卡片组件</h2>
  <div style="overflow: hidden; clear: both;">
    <!-- 商品1:使用默认操作按钮(加入购物车) -->
    <GoodsCard>
      <template #image>
        <img src="https://picsum.photos/240/180?random=1" alt="商品图片" style="width: 240px; height: 180px; object-fit: cover; border-radius: 8px;">
      </template>
      <template #title>
        小米手机 14 旗舰智能手机
      </template>
      <template #price>
        ¥ 4999
      </template>
      <!-- 未传递 #action 插槽,显示默认「加入购物车」按钮 -->
    </GoodsCard>

    <!-- 商品2:自定义操作按钮(立即购买 + 收藏) -->
    <GoodsCard>
      <template #image>
        <img src="https://picsum.photos/240/180?random=2" alt="商品图片" style="width: 240px; height: 180px; object-fit: cover; border-radius: 8px;">
      </template>
      <template #title>
        苹果 iPad Pro 平板电脑
      </template>
      <template #price>
        ¥ 7999
      </template>
      <!-- 自定义 #action 插槽内容,覆盖默认按钮 -->
      <template #action>
        <button style="width: 48%; padding: 8px 0; background: #0071e3; color: #fff; border: none; border-radius: 8px; cursor: pointer; margin-right: 4%;">
          立即购买
        </button>
        <button style="width: 48%; padding: 8px 0; background: #f0f0f0; color: #333; border: none; border-radius: 8px; cursor: pointer;">
          收藏
        </button>
      </template>
    </GoodsCard>
  </div>
</template>

<script setup>
import GoodsCard from './GoodsCard.vue';
</script>

3. 运行效果与说明

  • 商品1:操作按钮显示默认的「加入购物车」,快速实现基础功能。
  • 商品2:操作按钮显示「立即购买 + 收藏」,满足自定义需求。
  • 业务价值:通过具名插槽,打造了「通用可复用」的商品卡片组件,父组件可以根据不同商品场景,灵活配置各个区域的内容,既减少了重复代码,又保证了灵活性。

第四步:实现「作用域插槽」

核心需求:基于现有商品卡片组件优化,让子组件持有私有商品数据,通过作用域插槽传递给父组件,父组件自定义渲染格式(如给高价商品加「高端」标识、显示商品优惠信息)。

1. 修改子组件:定义作用域插槽,传递内部数据(GoodsCard.vue

子组件新增内部私有数据,通过「属性绑定」给插槽传递数据(:数据名="子组件内部数据"):

vue

<template>
  <div class="goods-card" style="width: 280px; border: 1px solid #f0f0f0; border-radius: 12px; padding: 16px; margin: 16px; float: left; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
    <!-- 作用域插槽:商品图片(暴露商品id和图片地址) -->
    <div class="goods-img" style="width: 100%; height: 180px; margin-bottom: 12px; text-align: center;">
      <slot name="image" :goodsId="goods.id" :imgUrl="goods.imgUrl" />
    </div>

    <!-- 作用域插槽:商品标题(暴露商品名称和价格) -->
    <div class="goods-title" style="font-size: 16px; font-weight: 500; margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
      <slot name="title" :goodsName="goods.name" :goodsPrice="goods.price" />
    </div>

    <!-- 作用域插槽:商品价格(暴露价格和优惠信息) -->
    <div class="goods-price" style="font-size: 18px; color: #ff4400; margin-bottom: 16px;">
      <slot name="price" :price="goods.price" :discount="goods.discount" />
    </div>

    <!-- 具名插槽:操作按钮(带默认内容) -->
    <div class="goods-actions" style="text-align: center;">
      <slot name="action">
        <!-- 默认内容:加入购物车按钮 -->
        <button style="width: 100%; padding: 8px 0; background: #ff4400; color: #fff; border: none; border-radius: 8px; cursor: pointer;">
          加入购物车
        </button>
      </slot>
    </div>
  </div>
</template>

<script setup>
// 子组件内部私有数据(模拟接口返回,父组件无法直接访问)
const goods = {
  id: 1001,
  name: "小米手机 14 旗舰智能手机",
  price: 4999,
  imgUrl: "https://picsum.photos/240/180?random=1",
  discount: "立减200元,支持分期免息"
};
</script>

2. 父组件:接收并使用作用域插槽数据(App.vue

父组件通过 template #插槽名="插槽数据对象" 接收子组件暴露的数据,支持解构赋值简化代码,自定义渲染逻辑:

vue

<template>
  <h2>进阶:作用域插槽演示(子组件供数,父组件定制渲染)</h2>
  <div style="overflow: hidden; clear: both; margin-top: 40px;">
    <GoodsCard>
      <!-- 接收图片插槽的作用域数据:slotProps(自定义名称,包含goodsId、imgUrl) -->
      <template #image="slotProps">
        <img :src="slotProps.imgUrl" :alt="'商品' + slotProps.goodsId" style="width: 240px; height: 180px; object-fit: cover; border-radius: 8px;">
        <!-- 利用子组件传递的goodsId,添加自定义标识 -->
        <span style="position: absolute; top: 8px; left: 8px; background: red; color: #fff; padding: 2px 8px; border-radius: 4px; z-index: 10;">
          编号:{{ slotProps.goodsId }}
        </span>
      </template>

      <!-- 接收标题插槽的作用域数据:解构赋值(更简洁,推荐) -->
      <template #title="{ goodsName, goodsPrice }">
        {{ goodsName }}
        <!-- 父组件自定义逻辑:价格高于4000加「高端」标识 -->
        <span v-if="goodsPrice > 4000" style="color: #ff4400; font-size: 12px; margin-left: 8px;">
          高端
        </span>
      </template>

      <!-- 接收价格插槽的作用域数据:结合优惠信息渲染 -->
      <template #price="{ price, discount }">
        <span>¥ {{ price }}</span>
        <!-- 渲染子组件传递的优惠信息,自定义样式 -->
        <p style="font-size: 12px; color: #999; margin-top: 4px; text-align: left;">
          {{ discount }}
        </p>
      </template>
    </GoodsCard>
  </div>
</template>

<script setup>
import GoodsCard from './components/GoodsCard.vue';
</script>

3. 运行效果与说明

  • 父组件成功获取子组件私有数据(goodsIddiscount 等),并实现自定义渲染(商品编号、高端标识、优惠信息);
  • 核心语法:子组件「属性绑定传数据」,父组件「插槽数据对象接收」,支持解构赋值简化代码;
  • 核心价值:通用组件(列表、卡片、表格)既保留内部数据逻辑,又开放渲染格式定制权,极大提升组件灵活性和复用性;
  • 注意:作用域插槽本质仍是具名 / 默认插槽,只是增加了「子向父」的数据传递能力。

三、总结(核心知识点回顾,加深记忆)

  1. 使用步骤
  • 子组件:用 <slot name="xxx"> 定义具名插槽(内部可写默认内容),用 :数据名="内部数据" 给插槽传递数据(作用域插槽);
  • 父组件:用 <template #xxx> 给指定具名插槽传内容,用 <template #xxx="slotProps"> 接收作用域插槽数据,未指定插槽名的内容默认插入到 <slot>(默认插槽)。
  1. 核心语法
  • v-slot:插槽名 可简写为 #插槽名,仅能用于 <template> 标签或组件标签上;
  • 作用域插槽数据支持解构赋值,可设置默认值(如 #title="{ goodsName = '默认商品', goodsPrice = 0 }")避免报错。
  1. 插槽体系
  • 基础层:默认插槽(单一区域)、具名插槽(多区域精准定制);
  • 增强层:插槽默认内容(提升健壮性)、作用域插槽(子供数 + 父定制,进阶核心)。

从零构建 Vue 弹窗组件

整体学习路线:简易弹窗 → 完善基础功能 → 组件内部状态管理 → 父→子传值 → 子→父传值 → 跨组件传值(最终目标)


步骤 1:搭建最基础的弹窗(静态结构,无交互)

目标:实现一个固定显示在页面中的弹窗,包含标题、内容、关闭按钮,掌握 Vue 组件的基本结构。

组件文件:BasicPopup.vue

<template>
  <!-- 弹窗外层容器(遮罩层) -->
  <div class="popup-mask">
    <!-- 弹窗主体 -->
    <div class="popup-content">
      <h3>简易弹窗</h3>
      <p>这是最基础的弹窗内容</p>
      <button>关闭</button>
    </div>
  </div>
</template>

<script setup>
// 现阶段无逻辑,仅搭建结构
</script>

<style scoped>
/* 遮罩层:占满整个屏幕,半透明背景 */
.popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 弹窗主体:白色背景,固定宽高,圆角 */
.popup-content {
  width: 400px;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  text-align: center;
}

/* 按钮样式 */
button {
  margin-top: 20px;
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

使用组件(App.vue

<template>
  <h1>弹窗学习演示</h1>
  <BasicPopup />
</template>

<script setup>
import BasicPopup from './components/BasicPopup.vue';
</script>

步骤 2:添加基础交互(控制弹窗显示/隐藏)

目标:通过「响应式状态」控制弹窗的显示与隐藏,给关闭按钮添加点击事件,掌握 ref 和事件绑定。

改造 BasicPopup.vue(新增响应式状态和点击事件)

<template>
  <!-- 用 v-if 控制弹窗是否显示 -->
  <div class="popup-mask" v-if="isShow">
    <div class="popup-content">
      <h3>简易弹窗</h3>
      <p>这是最基础的弹窗内容</p>
      <!-- 绑定关闭按钮点击事件 -->
      <button @click="closePopup">关闭</button>
    </div>
  </div>
</template>

<script setup>
// 1. 导入 ref 用于创建响应式状态
import { ref } from 'vue';

// 2. 定义响应式变量,控制弹窗显示/隐藏
const isShow = ref(true); // 初始值为 true,默认显示弹窗

// 3. 定义关闭弹窗的方法
const closePopup = () => {
  isShow.value = false; // 响应式变量修改需要通过 .value
};
</script>

<style scoped>
/* 样式同步骤 1,不变 */
.popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.popup-content {
  width: 400px;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  text-align: center;
}

button {
  margin-top: 20px;
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

补充:在 App.vue 添加「打开弹窗」按钮

我们知道,Vue 遵循「单向数据流」和「组件封装隔离」,子组件内部的方法 / 私有状态默认是对外隐藏的,外部父组件无法直接访问。

ref 就是打破这种 “隔离” 的合法方式(非侵入式),让父组件能够:

  1. 调用子组件通过 defineExpose 暴露的方法(如 openPopup 方法,用于打开弹窗)。
  2. 访问子组件通过 defineExpose 暴露的响应式状态(如弹窗内部的 isShow 状态)。
  3. 实现「父组件主动控制子组件」的交互场景(如主动打开 / 关闭弹窗、主动刷新子组件数据)。
<template>
  <h1>弹窗学习演示</h1>
  <!-- 新增打开弹窗按钮 -->
  <button @click="handleOpenPopup">打开弹窗</button>
  <BasicPopup ref="popupRef" />
</template>

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

// 获取弹窗组件实例
const popupRef = ref(null);

// 打开弹窗的方法(调用子组件的方法,后续步骤会完善)
const handleOpenPopup = () => {
  if (popupRef.value) {
    popupRef.value.openPopup();
  }
};
</script>

<style>
button {
  margin: 10px;
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

BasicPopup.vue 补充「打开弹窗」方法(暴露给父组件)

<template>
  <div class="popup-mask" v-if="isShow">
    <div class="popup-content">
      <h3>简易弹窗</h3>
      <p>这是最基础的弹窗内容</p>
      <button @click="closePopup">关闭</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const isShow = ref(false); // 初始值改为 false,默认隐藏

const closePopup = () => {
  isShow.value = false;
};

// 新增:打开弹窗的方法
const openPopup = () => {
  isShow.value = true;
};

// 暴露组件方法,让父组件可以调用(关键)
defineExpose({
  openPopup
});
</script>

<style scoped>
/* 样式同前 */
</style>

核心知识点

  1. ref 创建响应式状态,修改时需要 .value
  2. @click 事件绑定,触发自定义方法
  3. defineExpose 暴露组件内部方法/状态,供父组件调用
  4. v-if 控制元素的渲染与销毁(实现弹窗显示/隐藏)

步骤 3:父组件 → 子组件传值(Props 传递)

目标:父组件向弹窗组件传递「弹窗标题」和「弹窗内容」,掌握 defineProps 的使用,实现弹窗内容的动态化。

改造 BasicPopup.vue(接收父组件传递的参数)

<template>
  <div class="popup-mask" v-if="isShow">
    <div class="popup-content">
      <!-- 渲染父组件传递的标题 -->
      <h3>{{ popupTitle }}</h3>
      <!-- 渲染父组件传递的内容 -->
      <p>{{ popupContent }}</p>
      <button @click="closePopup">关闭</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 1. 定义 Props,接收父组件传递的值(指定类型和默认值)
const props = defineProps({
  // 弹窗标题
  popupTitle: {
    type: String,
    default: '默认弹窗标题' // 默认值,防止父组件未传递
  },
  // 弹窗内容
  popupContent: {
    type: String,
    default: '默认弹窗内容'
  }
});

const isShow = ref(false);

const closePopup = () => {
  isShow.value = false;
};

const openPopup = () => {
  isShow.value = true;
};

defineExpose({
  openPopup
});
</script>

<style scoped>
/* 样式同前 */
</style>

改造 App.vue(向子组件传递 Props 数据)

<template>
  <h1>弹窗学习演示</h1>
  <button @click="openPopup">打开弹窗</button>
  <!-- 向子组件传递 props 数据(静态传递 + 动态传递均可) -->
  <BasicPopup
    ref="popupRef"
    popupTitle="父组件传递的标题"
    :popupContent="dynamicContent"
  />
</template>

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

const popupRef = ref(null);
// 动态定义弹窗内容(也可以是静态字符串)
const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~');

const openPopup = () => {
  if (popupRef.value) {
    popupRef.value.openPopup();
  }
};
</script>

<style>
button {
  margin: 10px;
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

核心知识点

  1. defineProps 定义组件接收的参数,支持类型校验和默认值
  2. Props 传递规则:父组件通过「属性绑定」向子组件传值,子组件只读(不能修改 Props,遵循单向数据流)
  3. 静态传值(直接写字符串)直接把等号后面的内容作为纯字符串传递给子组件的 Props,Vue 不会对其做任何解析、计算,原样传递。动态传值(:xxx="变量") Vue 会先解析求值,再把结果传递给子组件的 Props。

步骤 4:子组件 → 父组件传值(Emits 事件派发)

目标:弹窗关闭时,向父组件传递「弹窗关闭的状态」和「自定义数据」,掌握 defineEmits 的使用,实现子向父的通信。

改造 BasicPopup.vue(派发事件给父组件)

<template>
  <div class="popup-mask" v-if="isShow">
    <div class="popup-content">
      <h3>{{ popupTitle }}</h3>
      <p>{{ popupContent }}</p>
      <!-- 关闭按钮点击时,触发事件派发 -->
      <button @click="handleClose">关闭</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 1. 定义 Props
const props = defineProps({
  popupTitle: {
    type: String,
    default: '默认弹窗标题'
  },
  popupContent: {
    type: String,
    default: '默认弹窗内容'
  }
});

// 2. 定义 Emits,声明要派发的事件(支持数组/对象格式,对象格式可校验)
const emit = defineEmits([
  'popup-close', // 弹窗关闭事件
  'send-data'    // 向父组件传递数据的事件
]);

const isShow = ref(false);

// 3. 改造关闭方法,派发事件给父组件
const handleClose = () => {
  isShow.value = false;
  
  // 派发「popup-close」事件,可携带参数(可选)
  emit('popup-close', {
    closeTime: new Date().toLocaleString(),
    message: '弹窗已正常关闭'
  });
  
  // 派发「send-data」事件,传递自定义数据
  emit('send-data', '这是子组件向父组件传递的额外数据');
};

const openPopup = () => {
  isShow.value = true;
};

defineExpose({
  openPopup
});
</script>

<style scoped>
/* 样式同前 */
</style>

改造 App.vue(监听子组件派发的事件)

<template>
  <h1>弹窗学习演示</h1>
  <button @click="openPopup">打开弹窗</button>
  <!-- 监听子组件派发的事件,绑定处理方法 -->
  <BasicPopup
    ref="popupRef"
    popupTitle="父组件传递的标题"
    :popupContent="dynamicContent"
    @popup-close="handlePopupClose"
    @send-data="handleReceiveData"
  />
</template>

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

const popupRef = ref(null);
const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~');

const openPopup = () => {
  if (popupRef.value) {
    popupRef.value.openPopup();
  }
};

// 1. 处理子组件派发的「popup-close」事件
const handlePopupClose = (closeInfo) => {
  console.log('接收弹窗关闭信息:', closeInfo);
  alert(`弹窗已关闭,关闭时间:${closeInfo.closeTime}`);
};

// 2. 处理子组件派发的「send-data」事件
const handleReceiveData = (data) => {
  console.log('接收子组件传递的数据:', data);
  alert(`收到子组件数据:${data}`);
};
</script>

<style>
button {
  margin: 10px;
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

核心知识点

  1. defineEmits 声明组件要派发的事件,遵循「事件名小写+短横线分隔」规范
  2. emit 方法用于派发事件,第一个参数是事件名,后续参数是要传递的数据
  3. 父组件通过 @事件名 监听子组件事件,处理方法的参数就是子组件传递的数据
  4. 单向数据流补充:子组件不能直接修改 Props,如需修改,可通过「子组件派发事件 → 父组件修改数据 → Props 重新传递」实现

步骤 5:跨组件传值(非父子组件,使用 Provide / Inject)

目标:实现「非父子组件」(如:Grandpa.vueParent.vuePopup.vue,爷爷组件向弹窗组件传值)的通信,掌握 provide / inject 的使用,这是 Vue 中跨组件传值的核心方案之一。

步骤 5.1:创建层级组件结构

├── App.vue(入口)
├── components/
│   ├── Grandpa.vue(爷爷组件,提供数据)
│   ├── Parent.vue(父组件,中间层级,无数据处理)
│   └── Popup.vue(弹窗组件,注入并使用数据)

步骤 5.2:爷爷组件 Grandpa.vue(提供数据,provide

<template>
  <div class="grandpa">
    <h2>爷爷组件</h2>
    <button @click="updateGlobalData">修改跨组件传递的数据</button>
    <!-- 引入父组件(中间层级) -->
    <Parent />
  </div>
</template>

<script setup>
import { ref, provide } from 'vue';
import Parent from './Parent.vue';

// 1. 定义要跨组件传递的响应式数据
const globalPopupConfig = ref({
  author: 'yyt',
  version: '1.0.0',
  theme: 'light',
  maxWidth: '500px'
});

// 2. 提供(provide)数据,供后代组件注入使用
// 第一个参数:注入标识(字符串/Symbol),第二个参数:要传递的数据
provide('popupGlobalConfig', globalPopupConfig);

// 3. 修改响应式数据(后代组件会同步更新)
const updateGlobalData = () => {
  globalPopupConfig.value = {
    ...globalPopupConfig.value,
    version: '2.0.0',
    theme: 'dark',
    updateTime: new Date().toLocaleString()
  };
};
</script>

<style scoped>
.grandpa {
  padding: 20px;
  border: 2px solid #666;
  border-radius: 8px;
  margin: 10px;
}

button {
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

步骤 5.3:父组件 Parent.vue(中间层级,仅做组件嵌套)

<template>
  <div class="parent">
    <h3>父组件(中间层级)</h3>
    <button @click="openPopup">打开跨组件传值的弹窗</button>
    <!-- 引入弹窗组件 -->
    <Popup ref="popupRef" />
  </div>
</template>

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

const popupRef = ref(null);

// 打开弹窗
const openPopup = () => {
  if (popupRef.value) {
    popupRef.value.openPopup();
  }
};
</script>

<style scoped>
.parent {
  padding: 20px;
  border: 2px solid #999;
  border-radius: 8px;
  margin: 10px;
  margin-top: 20px;
}

button {
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

步骤 5.4:弹窗组件 Popup.vue(注入数据,inject

<template>
  <div class="popup-mask" v-if="isShow" :style="{ backgroundColor: themeBg }">
    <div class="popup-content" :style="{ maxWidth: globalConfig.maxWidth, background: globalConfig.theme === 'dark' ? '#333' : '#fff', color: globalConfig.theme === 'dark' ? '#fff' : '#333' }">
      <h3>{{ popupTitle }}</h3>
      <p>{{ popupContent }}</p>
      <!-- 渲染跨组件传递的数据 -->
      <div class="global-info" style="margin: 15px 0; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
        <p>跨组件传递的配置:</p>
        <p>作者:{{ globalConfig.author }}</p>
        <p>版本:{{ globalConfig.version }}</p>
        <p>主题:{{ globalConfig.theme }}</p>
        <p v-if="globalConfig.updateTime">更新时间:{{ globalConfig.updateTime }}</p>
      </div>
      <button @click="handleClose">关闭</button>
    </div>
  </div>
</template>

<script setup>
import { ref, inject, computed } from 'vue';

// 1. 定义 Props
const props = defineProps({
  popupTitle: {
    type: String,
    default: '默认弹窗标题'
  },
  popupContent: {
    type: String,
    default: '默认弹窗内容'
  }
});

// 2. 注入(inject)爷爷组件提供的数据
// 第一个参数:注入标识(与 provide 一致),第二个参数:默认值(可选)
const globalConfig = inject('popupGlobalConfig', ref({ author: '默认作者', version: '0.0.1' }));

// 3. 基于注入的数据创建计算属性(可选,优化使用体验)
const themeBg = computed(() => {
  return globalConfig.value.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)';
});

// 4. 定义 Emits
const emit = defineEmits(['popup-close']);

const isShow = ref(false);

// 5. 关闭方法
const handleClose = () => {
  isShow.value = false;
  emit('popup-close', { message: '弹窗已关闭' });
};

// 6. 打开弹窗方法
const openPopup = () => {
  isShow.value = true;
};

// 7. 暴露方法
defineExpose({
  openPopup
});
</script>

<style scoped>
.popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.popup-content {
  padding: 20px;
  border-radius: 8px;
  text-align: center;
}

button {
  margin-top: 20px;
  padding: 8px 16px;
  background-color: #1890ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

步骤 5.5:入口 App.vue(引入爷爷组件)

<template>
  <h1>跨组件传值演示(Provide / Inject)</h1>
  <Grandpa />
</template>

<script setup>
import Grandpa from './components/Grandpa.vue';
</script>

核心知识点

  1. provide / inject 用于跨层级组件通信(无论层级多深),爷爷组件提供数据,后代组件注入使用
  2. 传递响应式数据:provide 时传递 ref/reactive 包装的数据,后代组件可感知数据变化(同步更新)
  3. 注入标识:建议使用 Symbol 避免命名冲突(生产环境推荐),本步骤为简化使用字符串标识
  4. 注入默认值:inject 第二个参数为默认值,防止祖先组件未提供数据时出现报错
  5. 与 Props/Emits 的区别:Props 适用于父子组件,provide/inject 适用于跨层级组件
❌