溪语流沙头像
【Web全栈进阶】Composition API与组件通信:props深入、emit、插槽封面图

【Web全栈进阶】Composition API与组件通信:props深入、emit、插槽

ArticleCard会写了,但组件之间还是“孤岛”——早报站的页面结构要正式开搭,先解决组件怎么说话:父传子(props)、子传父(emit)、注入内容(slot)、双向绑定(v-model)。

🎯 本篇产出:ArticleCard v2(收藏按钮)、SearchInput搜索框组件、插槽使用——三大通信姿势一次讲全。含代码约100行。


📌 太长不看版(给想快速上手的你)

项目信息一句话说明
本篇目标打通Vue组件之间的通信
代码行数~100行(含注释)
依赖无新增(脚手架已就位)
核心功能emit收藏 + v-model搜索 + 具名插槽
跑起来的命令npm run dev → http://localhost:5173
核心知识点单向数据流、defineEmits、v-model原理、slot
做完你能得到一套组件通信的完整姿势

一、概念课:组件通信的三种姿势 + 一条铁律

组件之间的关系只有一种:父子。通信方式就三种,外加一个表单专用语法糖:
在这里插入图片描述

🚨 先立那条铁律——单向数据流

📌 数据只有一个主人:谁拥有它,谁才能修改它。
子组件想改父组件的数据?把“请求”发上去(emit),让主人自己改。

🔄 对后端读者的翻译

Vue概念Python对应物说明
props函数参数(只读)父传子
emit事件通知父组件把handler传下来,子组件在恰当时机触发它——近似“把回调传下去”
slot模板里的“占位插槽”父组件注入内容

📌 数据流永远自上而下、事件永远自下而上——方向不乱,代码就乱不了。这和第12篇“底层抛异常、顶层接异常”的分层纪律是同一个思想。


二、第1步:ArticleCard v2——加收藏按钮(emit实战)

给卡片升级:点收藏 → 卡片自己不知道收藏存哪(那是父组件和将来的API的事),它只负责把事件报上去。

src/components/ArticleCard.vue(v2关键改动):

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

const props = defineProps({
  id: Number,                 // 新增:上报时要带上“是谁”(id就是载荷的一部分)
  title: String,
  summary: String,
  url: String,
})

// ① 声明本组件会发出的事件——声明过的才算数(第六节①会验证这点)
const emit = defineEmits(['favorite'])

const favorited = ref(false)

function onFavorite() {
  favorited.value = !favorited.value
  // ② 上报事件 + 载荷(对象里带id和最新状态)
  emit('favorite', { id: props.id, state: favorited.value })
}
</script>

<template>
  <article class="card">
    <h2><a :href="url" target="_blank">{{ title }}</a></h2>
    <p>{{ summary }}</p>
    <button @click="onFavorite">
      {{ favorited ? '★ 已收藏' : '☆ 收藏' }}
    </button>
    <!-- ③ 具名插槽出口:父组件可以往这里注入内容(如“阅读原文”链接) -->
    <slot name="actions"></slot>
  </article>
</template>

📌 两个关键点

#关键点说明
①defineEmits(['favorite'])声明事件清单声明即契约,父组件按这个名字监听
②emit('favorite', 载荷)把id和状态一起报上去子组件不决定“收藏后干嘛”,只负责“告诉爸爸:用户收藏了id=3的文章”

💡 <slot name="actions">是插槽出口:组件留好"插座",父组件往里面插内容(见第3步)。


三、第2步:App.vue——监听事件、持有状态

src/App.vue(关键改动):

<script setup>
import { ref } from 'vue'
import ArticleCard from './components/ArticleCard.vue'
import SearchInput from './components/SearchInput.vue'

const articles = [
  { id: 1, title: '重构教会我的事', summary: '……', url: '#' },
  // ... 同第09篇
]

// 收藏计数:状态的主人在这里(App层)
const favorites = ref(0)

function handleFavorite({ id, state }) {
  favorites.value += state ? 1 : -1
  console.log('收藏事件:', id, state)   // 第13篇这里换成调API
}

const keyword = ref('')
</script>

<template>
  <h1>Python 早报</h1>
  <SearchInput v-model="keyword" />
  <p v-if="keyword">正在搜索:{{ keyword }}(第13篇接真搜索)</p>
  <p>已收藏 {{ favorites }} 篇</p>

  <ArticleCard
    v-for="a in articles"
    :key="a.id"
    v-bind="a"
    @favorite="handleFavorite"
  >
    <!-- 具名插槽的使用:往actions插座里插入“阅读原文”链接 -->
    <template #actions>
      <a :href="a.url" target="_blank">阅读原文 ↗</a>
    </template>
  </ArticleCard>
</template>

🎯 看清楚数据的流向

卡片里点收藏 → emit上报 → App的handleFavorite修改favorites → 顶部的“已收藏N篇”自动更新

📌 数据的主人(App)始终只有一个,修改永远发生在主人手里——单向数据流完整闭环。

💡 v-bind="a"的妙用

把对象的所有字段展开传给props的简写(等价于:id="a.id" :title="a.title"……),列表场景省一大截代码。


四、第3步:SearchInput——v-model的双向绑定原理

v-model是“双向绑定”语法糖。先拆穿它的真面目:

v-model="keyword"
    ≡  :model-value="keyword"  +  @update:model-value="keyword = $event"

📌 一个输入框组件要实现v-model,只需遵守两条约定:prop叫modelValue,事件叫update:modelValue。

src/components/SearchInput.vue:

<script setup>
defineProps({ modelValue: String })               // 约定1:prop名
const emit = defineEmits(['update:modelValue'])   // 约定2:事件名

function onInput(e) {
  emit('update:modelValue', e.target.value)       // 输入 → 上报给主人
}
</script>

<template>
  <input type="text" :value="modelValue"
         @input="onInput"
         placeholder="搜索文章…" />
</template>

🎯 数据流依旧单向

输入事件 → emit上报 → App改keyword → keyword流回:value

📌 “双向绑定”是错觉,底下还是单向流 + 语法糖——理解这一点,将来调试任何v-model问题都不慌(第六节④)。

✅ 浏览器实测

  • 搜索框打字 → “正在搜索:xxx”实时跟上;
  • 点卡片收藏 → 顶部计数+1。

🎉 全部无刷新、无后端——纯前端状态流的胜利。


五、验收清单

1. 点收藏 → ★切换 + 顶部“已收藏N篇”实时更新
2. 连点两次同一张卡 → 计数先+1再-1(状态上报正确)
3. 搜索框输入 → “正在搜索”文案实时跟上
4. 卡片底部出现“阅读原文”链接(具名插槽生效)
5. 控制台无Vue warn(事件已声明、props未被修改)
6. 全程无报错后提交Git
git add .
git commit -m "组件通信:emit收藏 + v-model搜索 + 具名插槽"

六、常见报错:这6个,组件通信的标配(重点!)

① 控制台警告Component emitted event "favorite" but it is neither declared nor listened to

🔍 原因:emit了没在defineEmits声明的事件(或父组件没监听)。

✅ 解法:defineEmits声明 + 父组件@favorite监听,两头对齐。

📌 事件和props一样是契约,先声明后使用。


② 父组件监听没反应:@favorite写了但handler不触发

🔍 原因:事件名拼写不一致——emit('favorite')与@favorite的大小写/拼写差一个字符。

✅ 解法:统一用camelCase命名。

📌 事件名是字符串,字符串的匹配没有任何容错。


③ [Vue warn]: Component is mutating props

🔍 原因:子组件直接改了props(比如props.summary = 'xx')——违反单向数据流。

✅ 解法:

  • 想改数据?emit上报让父组件改;
  • 只想内部用?先拷贝成本地ref。

④ v-model绑定的输入框不响应 / 输入后立刻“弹回”

🔍 原因:子组件没emit update:modelValue(或prop名不是modelValue)——输入了但主人没收到,value被旧值弹回。

✅ 解法:对照第4节的两条约定逐字检查。

📌 v-model不是魔法,是两条约定。


⑤ 插槽内容死活不显示

🔍 原因:子组件模板里没放<slot />出口——内容没地方插。

✅ 解法:检查子组件有没有“插座”;具名插槽要配对:<slot name="actions"> ↔ <template #actions>。


⑥ 具名插槽内容显示在了错误位置 / 和默认内容混在一起

🔍 原因:#actions写成了普通内容,或子组件默认slot与具名slot混淆。

✅ 解法:

  • 具名插槽一律用<template #名字>包裹;
  • 默认内容放<slot>(不带name)。

七、课后练习

#练习难度提示
1状态提升:把点赞的liked状态也提升到App(favorites列表存id),刷新页面后计数不丢⭐⭐⭐状态提升是组件设计的第一课
2删除卡片:给卡片加删除按钮,emit('remove', id),App里从数组移除⭐⭐复习“主人改数据”
3双v-model:搜索框加“区分大小写”复选框⭐⭐⭐:checked + @change,或v-model第二个绑定
4(选做)给emit加校验:v-model组件的modelValue prop加required: true与类型校验⭐⭐props深入的最后一块拼图

📦 配套代码

完整组件已上传Git(python_daily/,node_modules不入仓):【gitee仓库地址】

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/ntnmywyx/article/details/167282854

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--