第7次 · 组件基础:props 与 emits 父子通信
第7次 · 组件基础:props 与 emits 父子通信
第1/7页 · 先知道今天要做什么
到上一课为止,你写的所有东西都挤在一个 App.vue 里。页面一长,问题马上出来:同一个“学员卡片”要复制五遍、改一个样式要改五处、数据逻辑和显示逻辑混在一起。组件就是来解决“重复”的:把一段可复用的界面 + 它的数据打包成一个独立文件,需要的地方像用标签一样用它。
组件用起来像标签,那就必然要解决两个方向的通信:
- 父 → 子:父组件把数据交给子组件,子组件用
props接收(只读); - 子 → 父:子组件里有事发生(点了删除、改了值),用
emits通知父组件。
今天把这两个方向讲清楚。这是 Vue 里最重要的一次思维转变:从“写页面”变成“拼组件”。
今天的场景
学员管理列表。父组件持有学员数组,子组件 StudentCard 只负责显示一个学员并在被点击时发出通知。父组件负责处理“删除”和“切换提交状态”。子组件不碰数组,只负责显示和喊话——这是组件设计的第一条纪律。
下课时拿出这三样结果
| 做到什么 | 在哪里看 | 完成的证据 |
|---|---|---|
会用 props 传数据 |
每张学员卡片 | 卡片上的姓名、班级、状态都来自父组件传来的对象,子组件里没有任何写死的数据。 |
会用 defineEmits 派发事件 |
卡片上的按钮 | 点“删除”,父组件的列表真的少一条;点“切换状态”,标签真的换颜色。 |
| 知道为什么子组件不能直接改 props | 代码与讲解 | 能说出“props 是只读的”,以及为什么要把修改动作交给父组件。 |
你只需要带着这些基础来
会 ref、computed、v-if、v-for、@click;第 6 课的工程还能跑。
本课时 45 分钟怎么上
| 时间 | 老师带着做 | 学生留下什么 |
|---|---|---|
| 0—6 分钟 | 展示一段“复制了五遍的卡片代码”,让学生说出难维护在哪。 | 认同“组件是为了复用与隔离”。 |
| 6—20 分钟 | 讲 defineProps:声明方式、类型校验、默认值、required;讲为什么只读。 |
笔记里写下 props 声明的三种写法。 |
| 20—34 分钟 | 讲 defineEmits:声明、调用、父组件监听;讲“子组件不碰父数据”的纪律。 |
能说出 emit 的两个参数(事件名、载荷)。 |
| 34—42 分钟 | 粘贴本课 App.vue,点按钮验证父子通信。 |
删除与切换都能生效。 |
| 42—45 分钟 | 抽查:问“子组件为什么不直接删数组里的项”。 | 全员提交。 |
从哪里开始
复制第 6 课的工程,改名 lesson-vue-07。不需要新增依赖。
今天先掌握到这里
插槽(slot)是第 8 课的内容。今天先把“传数据进去、传事件出来”练熟。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档
第2/7页 · 把新词说清楚
新词与本课含义
| 名字 | 用熟悉的话理解 | 本课里长什么样 |
|---|---|---|
| 组件 | 一段界面 + 它的数据的打包,可以像标签一样重复使用。 | <StudentCard :student="s" /> |
| 父子组件 | 用了别人的组件,自己就是父;被用的那个就是子。 | App 是父,StudentCard 是子 |
props |
父组件传给子组件的数据参数,子组件只读。 | defineProps({ student: Object }) |
emits |
子组件向父组件报告事件的通道。 | defineEmits(['remove', 'toggle']) |
| 载荷(payload) | 事件里带出去的数据,比如“要删哪一条”。 | emit('remove', id) 里的 id |
props:父传子,只读
在子组件里声明:
<script setup>
// 写法一:数组式,只写名字(简单,但不推荐在生产里用)
const props = defineProps(['student', 'index'])
// 写法二:对象式,带类型(推荐)
const props = defineProps({
student: { type: Object, required: true },
index: { type: Number, default: 0 }
})
// 写法三:TypeScript 泛型式(本课程用 JS,了解即可)
// const props = defineProps<{ student: Student }>()
</script>
父组件传值:
<StudentCard :student="s" :index="i" />
四个必须记住的点:
- 传字符串字面量用
name="张三";传变量/数字/对象/布尔必须加冒号:student="s"。冒号就是v-bind的简写。 - props 是只读的。在子组件里写
props.student = xxx会在控制台报错。 - 为什么只读?因为同一个子组件可能被父组件用很多次,如果子组件能改 props,父组件的数据就会被“别人家的孩子”悄悄改掉,出了问题根本找不到是谁干的。
- 想显示加工后的值,用
computed或局部ref拷贝一份,不要改原值。
emits:子传父,靠“喊话”
子组件里:
<script setup>
const emit = defineEmits(['remove', 'toggle'])
function onDelete() {
emit('remove', props.student.id) // 事件名 + 要带出去的数据
}
</script>
父组件里:
<StudentCard
:student="s"
@remove="onRemove"
@toggle="onToggle"
/>
function onRemove(id) {
list.value = list.value.filter(s => s.id !== id)
}
注意命名习惯: 事件名建议用短横线或小驼峰(remove、item-click),不要用大写字母开头(HTML 属性不区分大小写,@Remove 会失效)。
一条纪律:数据归谁管,就由谁改
| 做法 | 后果 |
|---|---|
父组件持有数组,子组件只 emit |
数据只有一处修改入口,出问题一眼定位。 |
| 子组件也直接改数组 | 谁改的不知道,调试变成灾难。 |
记住这句话:谁拥有的数据,谁负责修改。 子组件要用,就通过 props 拿;子组件想改,就通过 emit 请求父组件改。这就是 Vue 里常说的“单向数据流”。
props 与 emits 对照表
| props | emits | |
|---|---|---|
| 方向 | 父 → 子 | 子 → 父 |
| 载体 | 属性值 | 事件 |
| 子组件里怎么写 | defineProps({...}) |
defineEmits([...]) |
| 子组件里怎么用 | props.xxx |
emit('事件名', 数据) |
| 父组件里怎么写 | :xxx="值" |
@事件名="处理函数" |
| 能不能改 | 不能(只读) | 不适用(只是通知) |
一个必须知道的补充:v-model 在组件上也是这项技术
上一课用的 <el-input v-model="form.name" />,在 el-input 内部其实就是:
- 通过 props 接收
modelValue; - 用户输入时
emit('update:modelValue', 新值)。
所以 v-model 不是魔法,它就是“props 进 + emits 出”的封装。理解了这一点,你自己写的组件也能支持 v-model。
只看这几行:一个最小子组件
<!-- 真实项目里子组件通常写在单独的 StudentCard.vue 文件里;
本课为了满足“App.vue 单文件粘贴即用”,在 App.vue 内用 h() 渲染函数定义,
两者的 props / emits 声明方式完全相同。 -->
<script setup>
import { defineComponent, h } from 'vue'
const StudentCard = defineComponent({
props: { name: { type: String, required: true } },
emits: ['remove'],
setup(props, { emit }) {
return () => h('div', { class: 'card' }, [
h('span', null, props.name), // 用 props
h('button', { onClick: () => emit('remove') }, '删除') // 发事件
])
}
})
</script>
<template>
<StudentCard name="张三" @remove="() => console.log('父组件收到删除请求')" />
</template>
先口头预测:如果子组件里写了 props.name = '李四',页面会变成李四吗?
不会。 Vue 会警告“Set operation on key “name” failed: target is readonly”,页面显示的还是父组件传进来的值。这就是“只读”的实际表现。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档
第3/7页 · 先看一个小例子
标准写法(真实项目)
真实项目里,子组件放在单独文件 src/components/StudentCard.vue:
<!-- src/components/StudentCard.vue -->
<script setup>
const props = defineProps({
student: { type: Object, required: true },
index: { type: Number, default: 0 }
})
const emit = defineEmits(['remove', 'toggle'])
</script>
<template>
<div class="stu">
<div class="row">
<span class="name">{{ index + 1 }}. {{ student.name }}</span>
<el-tag :type="student.done ? 'success' : 'info'" size="small">
{{ student.done ? '已提交' : '未提交' }}
</el-tag>
</div>
<p class="cls">{{ student.cls }}</p>
<p>得分:{{ student.score ?? '—' }}</p>
<el-button size="small" @click="emit('toggle', student.id)">切换状态</el-button>
<el-button size="small" type="danger" @click="emit('remove', student.id)">删除</el-button>
</div>
</template>
父组件 App.vue 里这样用:
<StudentCard
v-for="(s, i) in list" :key="s.id"
:student="s" :index="i"
@toggle="onToggle" @remove="onRemove"
/>
本课实际使用的 App.vue(单文件,含内联子组件)
因为课程要求“每课只有一个 App.vue、粘贴即可运行”,下面把上面的子组件用 h() 渲染函数写进同一个文件。props 与 emits 的声明、父组件的监听方式,与标准写法完全一致,只有“怎么描述界面”这一步从模板换成了 h()。
<script setup>
import { ref, computed, defineComponent, h } from 'vue'
import { ElCard, ElTag, ElButton, ElAlert, ElMessage, ElMessageBox, ElEmpty, ElInput, ElSwitch } from 'element-plus'
import 'element-plus/dist/index.css'
/* ============ 子组件:StudentCard(本课的主角) ============ */
const StudentCard = defineComponent({
name: 'StudentCard',
// ① props:父组件传进来的数据,只读
props: {
student: { type: Object, required: true },
index: { type: Number, default: 0 }
},
// ② emits:子组件可以向父组件报告的事件
emits: ['remove', 'toggle'],
setup(props, { emit }) {
return () => h(ElCard, { class: 'stu', shadow: 'hover' }, {
default: () => [
h('div', { class: 'row' }, [
h('span', { class: 'name' }, `${props.index + 1}. ${props.student.name}`),
h(ElTag, { type: props.student.done ? 'success' : 'info', size: 'small' },
() => (props.student.done ? '已提交' : '未提交'))
]),
h('p', { class: 'cls' }, props.student.cls),
h('p', null, `得分:${props.student.score ?? '—'}`),
h('div', { class: 'ops' }, [
h(ElButton, { size: 'small', onClick: () => emit('toggle', props.student.id) }, () => '切换状态'),
h(ElButton, { size: 'small', type: 'danger', onClick: () => emit('remove', props.student.id) }, () => '删除')
])
]
})
}
})
/* ============ 父组件:负责持有数据、处理事件 ============ */
const list = ref([ /* ...5 位学员... */ ])
const keyword = ref('')
const onlyTodo = ref(false)
const visible = computed(() => {
let arr = list.value
if (onlyTodo.value) arr = arr.filter(s => !s.done)
if (keyword.value) arr = arr.filter(s => s.name.includes(keyword.value))
return arr
})
const stat = computed(() => {
const done = list.value.filter(s => s.done).length
return { total: list.value.length, done }
})
// 子组件喊“toggle”,父组件在这里改数据
function onToggle(id) {
const s = list.value.find(x => x.id === id)
if (s) s.done = !s.done
}
// 子组件喊“remove”,父组件在这里删数据
function onRemove(id) {
const s = list.value.find(x => x.id === id)
ElMessageBox.confirm(`确定删除「${s?.name}」吗?`, '提示', { type: 'warning' })
.then(() => {
list.value = list.value.filter(x => x.id !== id)
ElMessage.success('已删除')
})
.catch(() => ElMessage.info('已取消'))
}
function addOne() {
const id = Date.now()
list.value.push({ id, name: '新同学' + (list.value.length + 1), cls: '计算机1班', done: false, score: null })
ElMessage.success('已新增一条,观察列表自动多出一张卡片')
}
</script>
<template>
<div class="page">
<h1 class="title">学员管理(父组件持数据 · 子组件只显示)</h1>
<el-card class="block">
<div class="toolbar">
<el-input v-model="keyword" placeholder="按姓名搜索" clearable style="width: 200px" />
<el-switch v-model="onlyTodo" active-text="只看未提交" />
<el-button type="primary" @click="addOne">新增一条</el-button>
<span class="stat">共 {{ stat.total }} 人,已提交 {{ stat.done }} 人</span>
</div>
</el-card>
<el-empty v-if="visible.length === 0" description="没有符合条件的学员" />
<div v-else class="grid">
<!-- 父组件把数据通过 props 传进去,把事件监听起来 -->
<StudentCard
v-for="(s, i) in visible"
:key="s.id"
:student="s"
:index="i"
@toggle="onToggle"
@remove="onRemove"
/>
</div>
<el-alert
title="注意:子组件里没有 list 这个变量,它既不能读也不能改。所有数据变化都由父组件的 onToggle / onRemove 完成。"
type="info" :closable="false" show-icon class="tip"
/>
</div>
</template>
<style>
html, body, #app {
margin: 0; padding: 0; max-width: none; width: auto; height: auto;
display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>
<style scoped>
.page { max-width: 1000px; margin: 28px auto; padding: 0 16px; }
.title { font-size: 22px; color: #303133; margin: 0 0 18px; }
.block { margin-bottom: 16px; }
.toolbar { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.stat { color: #606266; font-size: 14px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
:deep(.stu .row) { display: flex; justify-content: space-between; align-items: center; }
:deep(.stu .name) { font-weight: 600; color: #303133; }
:deep(.stu .cls) { color: #909399; font-size: 13px; margin: 6px 0; }
:deep(.stu .ops) { margin-top: 8px; }
.tip { margin-top: 16px; }
@media (max-width: 820px) { .grid { grid-template-columns: 1fr; } }
</style>
预期看见什么
- 顶部工具条:搜索框、只看未提交开关、新增按钮、统计文字。
- 下面三列学员卡片,每张卡片右侧两个按钮:“切换状态”和“删除”。
- 点“切换状态”,头部的“已提交 N 人”数字跟着变——因为改的是父组件的数据。
- 点“删除”,弹出确认框;确认后卡片消失,统计数字 -1。
- 搜索框输入“张”,只剩包含“张”的卡片;此时清空搜索,卡片全部回来。
先猜一猜,读完代码再回答
子组件里的按钮是这样写的:
h(ElButton, { size: 'small', onClick: () => emit('toggle', props.student.id) }, () => '切换状态')
为什么这里不直接写 props.student.done = !props.student.done?
两个原因:
- props 是只读的,这样写会直接报错;
- 即使能改也是错的。父组件的统计数据
stat、筛选结果visible都是从list派生的,子组件偷偷改了对象内部字段,虽然数据会变,但数据的“修改入口”散落在各处——项目一大,就没人知道一行数据显示异常到底是谁改的。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档
第4/7页 · 一步一步做出来
第1步:先把“复制五遍”的问题摆出来
在 App.vue 里粘五份几乎一样的卡片代码(只改姓名),然后改一下卡片里的一个样式(例如 .name 改成红色)。预期:你要改五处。这一步的目的是让你自己有拆组件的动力。
第2步:拆出第一个子组件(标准两文件写法)
新建 src/components/StudentCard.vue,只放显示逻辑;App.vue 里用 <StudentCard :student="s" /> 引用。预期:页面正常,卡片样式只写了一遍,改样式只改一个文件。
第3步:故意违反“只读”纪律
在子组件里加一句 props.student.name = '哈哈'。预期:Console 出现 Set operation on key "name" failed: target is readonly 警告,页面不变。看完就删掉这行。
第4步:用 emit 正确地改
子组件里:
const emit = defineEmits(['toggle'])
// 按钮换成:
@click="emit('toggle', student.id)"
父组件里:
function onToggle(id) {
const s = list.value.find(x => x.id === id)
if (s) s.done = !s.done
}
预期:点按钮,标签颜色变化,统计数字同步变化。
第5步:整段替换为本课完整版
把第 3 页的完整 App.vue 覆盖 src/App.vue(本课用内联子组件版本)。确认:
- 卡片能渲染;
- 切换状态、删除都生效;
- 搜索与筛选正常。
第6步:看一次 props 的校验
把父组件传的 :index="i" 删掉,只留 :student="s"。预期:仍然能跑(因为 index 有默认值 0),但卡片序号全变成 1.。这告诉你:有默认值的 props 可以不传。
再把 :student="s" 删掉。预期:Console 报 Missing required prop: "student" 并可能报错。这说明 required: true 是真的在起作用。
本课文件速查
| 位置 | 本次作用 |
|---|---|
子组件 props |
接收 student 与 index,只读。 |
子组件 emits: ['remove','toggle'] |
声明它能向父组件报告的两件事。 |
emit('toggle', props.student.id) |
把“要改哪一条”告诉父组件。 |
父组件 onToggle / onRemove |
唯一的修改入口。 |
:student="s" / @toggle="onToggle" |
父组件与子组件的两条通道。 |
完成修改后检查
| 操作或输入 | 预期观察 |
|---|---|
| 点“切换状态” | 标签变化 + 头部统计变化。 |
| 点“删除”并确认 | 卡片消失,总数 -1。 |
| 点“删除”后取消 | 卡片保留,提示“已取消”。 |
删除 :index="i" |
序号全变成 1(用了默认值)。 |
| 在子组件里改 props | Console 报只读警告,页面不变。 |
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档
第5/7页 · 换成自己的代码
你的任务
- 换成你自己的数据与卡片:把
list换成 6 条你自己的记录(例如图书、商品、任务),卡片上显示的字段改成你自己的。 - 再加一个 props:给子组件加一个
showScore(布尔,默认true),控制卡片里是否显示得分那一行。父组件用一个开关统一控制。 - 再加一个 emit:给子组件加事件
star,卡片上加一个“收藏”按钮,点它时把整条记录作为载荷传给父组件(而不是只传 id),父组件据此把该条记录的starred字段置为true并弹提示。 - 把卡片拆成真正的两个文件:新建
src/components/StudentCard.vue,把子组件搬进去(模板写法),App.vue里import进来用。这一步是本课的重点作业,做完后页面行为应和之前完全一致。 - 验证只读:在子组件里写一行试图改 props 的代码,把控制台警告截图/抄下来,然后删掉。
按这张操作单完成
| 步骤 | 改哪里 | 运行后应该看到什么 |
|---|---|---|
| 1 换数据 | 父组件 list |
6 条自己的记录。 |
| 2 加 props | 子组件 props + 模板 | 关掉开关后卡片上不再显示得分行。 |
| 3 加 emit | 子组件按钮 + 父组件处理 | 点收藏后父组件数据里该条 starred 变 true。 |
| 4 拆文件 | 新建 components/StudentCard.vue |
行为与拆之前完全一致。 |
| 5 验证只读 | 临时写一行改 props | Console 出现 readonly 警告。 |
最低完成标准:先把这三项做实
| 检查项 | 通过条件 |
|---|---|
| props 传值 | 子组件里没有任何写死的数据,全部来自 props。 |
| emits 通信 | 至少两个自定义事件生效,且父组件能收到载荷。 |
| 单向数据流 | 子组件代码里找不到任何对父组件数据的直接读写。 |
在本地写练习记录
- 画一张父子通信图:标出哪条线是 props、哪条线是 emit。
- 抄下
Missing required prop的完整报错原文。 - 写下拆文件前后的对比感受:哪个更好维护,为什么。
最后交什么
提交 src/App.vue(以及你拆出的 src/components/StudentCard.vue)与 练习记录.md。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档
第6/7页 · 出错时从哪里查
把现象和检查动作对上
| 看到什么现象 / 报错 | 先检查哪里 | 修好后怎样确认 |
|---|---|---|
| 组件没显示,页面白屏 | 忘了 import 子组件(或用内联组件时变量名与模板里不一致)。 |
加上 import / 改对名字后正常渲染。 |
props 拿到的是字符串 "1" 而不是数字 1 |
传值时忘了加冒号,写成了 index="1"。 |
改成 :index="1"。 |
Console 报 Missing required prop |
父组件没传这个必填的 props。 | 补上传值。 |
Set operation on key "xxx" failed: target is readonly |
子组件里直接改了 props。 | 改成 emit,由父组件修改。 |
| 父组件监听不到事件 | 事件名大小写不一致,或子组件忘了在 emits 里声明。 |
统一改成小写短横线后生效。 |
| 点按钮触发了很多次事件 | 在 h() 里写成 onClick: emit('x')(直接调用)而不是 onClick: () => emit('x')。 |
改成箭头函数包一层。 |
| 列表刷新后卡片顺序乱、输入框串行 | v-for 的 key 用了下标。 |
改成 :key="item.id"。 |
排错口诀
传字符串不加冒号,传变量必须加冒号; props 只读别去改,想改就 emit 喊一声; 父组件没监听,子组件喊破喉咙也没用;
h()里绑事件要包一层箭头函数,别直接调用。
不要用这些办法“修复”
- 不要为了让子组件能改数据,用
defineModel或直接把数组provide下去绕过 props——在数据归属不清的项目里,这类“抄近路”会在后面第 4 阶段引发大量难查的问题。 - 不要把所有数据都塞进一个超大的
props对象(例如传整个 store 进去)。props 应该只传这个组件真正需要的东西。 - 不要因为“报 required 警告”就把
required: true删掉了事,先想清楚这个数据是不是真的必须传。
需要老师帮忙时交这些
子组件的 props 声明、父组件的传值写法、@事件名 的监听写法、Console 里完整的警告或报错原文、你点了什么、期望发生什么、实际发生什么。
老师巡视时只问三句话
- 子组件里有没有直接改 props 的地方?(能自查并指出没有)
- 这个按钮点下去,数据是谁改的?(能指出父组件里的处理函数)
- 如果这个卡片要用在别的页面,你能不能说清它需要哪些 props?(能一口气报出 props 清单)
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档
第7/7页 · 用自己的话讲一遍
回到第 3 页:你刚才猜对了吗?
为什么子组件不能直接改 props?
参考解释:props 由父组件传入,是父组件数据的“窗口”,不是子组件的私有财产。如果允许子组件改,同一个数据可能有多个修改入口,出了问题无法定位,而且父组件里的派生数据(统计、筛选结果)也不会按预期更新。Vue 直接把 props 设成只读,从机制上堵住这条路。
回答本课的两个问题
| 问题 | 参考解释 |
|---|---|
| props 和 emits 分别解决什么问题? | props 解决“父组件怎么把数据给子组件”,emits 解决“子组件怎么把发生的事情告诉父组件”。一个向下,一个向上。 |
为什么 v-model 能在组件上用? |
因为它就是 props + emits 的组合:内部接收 modelValue,用户操作时 emit('update:modelValue', 新值)。 |
五个必须能答上来的问题
| 考考自己 | 可以这样通俗回答 |
|---|---|
:index="i" 和 index="i" 有什么区别? |
前者传的是变量 i 的值(数字),后者传的是字符串 "i"。 |
| props 声明用数组式还是对象式? | 对象式,因为可以写类型、默认值、是否必填,出错时 Vue 会提醒。 |
| 子组件怎么告诉父组件“我要删这条”? | emit('remove', id),父组件用 @remove="onRemove" 接收。 |
| 为什么事件名不要用大写开头? | HTML 属性不区分大小写,@Remove 会被当成 @remove,容易莫名其妙失效。 |
| 组件拆分的第一条好处是什么? | 复用。同一段界面只写一次,改一处全生效。 |
一分钟现场演示指南
- 展示复用:说明同一份卡片代码只写了一次,页面却出现很多张。
- 展示父子通信:指着三点讲清 —— 数据从父组件进来(props),点击事件从子组件出去(emit),父组件改完数据再回到界面。
- 展示纪律:打开子组件源码,指出里面没有
list这个变量。 - 收尾一句:“组件之间不互相改数据,只通过 props 和 emits 说话。”
交作业前最后检查
src/App.vue与练习记录.md已保存。- 子组件里没有任何
props.xxx = ...的写法。 - 至少两个自定义事件能触发并生效。
- 拆文件后页面行为与拆之前一致。
- Console 无红色报错(
readonly那条是你故意做的实验,记得删掉)。
课后再做一个小对照
尝试在同一个父组件里用两次同一个子组件,一次传 :showScore="true"、一次传 :showScore="false"。观察:同一个组件文件,靠 props 就能表现出两种形态。写下一句话总结:props 让“同一个组件”有了多大的表达力(提示:这就是后面做通用组件的思路)。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(组件基础 / Props / 自定义事件章节)· 组件依据 Element Plus 2.8 官方文档