孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第7次 · 组件基础:props 与 emits 父子通信
模块总览

第7次 · 组件基础:props 与 emits 父子通信

2026-09-13 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 07-组件基础props与emits父子通信.zip
下载 ZIP 压缩包

第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" />

四个必须记住的点:

  1. 传字符串字面量用 name="张三";传变量/数字/对象/布尔必须加冒号 :student="s"。冒号就是 v-bind 的简写。
  2. props 是只读的。在子组件里写 props.student = xxx 会在控制台报错。
  3. 为什么只读?因为同一个子组件可能被父组件用很多次,如果子组件能改 props,父组件的数据就会被“别人家的孩子”悄悄改掉,出了问题根本找不到是谁干的。
  4. 想显示加工后的值,用 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>

预期看见什么

  1. 顶部工具条:搜索框、只看未提交开关、新增按钮、统计文字。
  2. 下面三列学员卡片,每张卡片右侧两个按钮:“切换状态”和“删除”。
  3. 点“切换状态”,头部的“已提交 N 人”数字跟着变——因为改的是父组件的数据。
  4. 点“删除”,弹出确认框;确认后卡片消失,统计数字 -1。
  5. 搜索框输入“张”,只剩包含“张”的卡片;此时清空搜索,卡片全部回来。

先猜一猜,读完代码再回答

子组件里的按钮是这样写的:

h(ElButton, { size: 'small', onClick: () => emit('toggle', props.student.id) }, () => '切换状态')

为什么这里不直接写 props.student.done = !props.student.done?

两个原因:

  1. props 是只读的,这样写会直接报错;
  2. 即使能改也是错的。父组件的统计数据 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页 · 换成自己的代码

你的任务

  1. 换成你自己的数据与卡片:把 list 换成 6 条你自己的记录(例如图书、商品、任务),卡片上显示的字段改成你自己的。
  2. 再加一个 props:给子组件加一个 showScore(布尔,默认 true),控制卡片里是否显示得分那一行。父组件用一个开关统一控制。
  3. 再加一个 emit:给子组件加事件 star,卡片上加一个“收藏”按钮,点它时把整条记录作为载荷传给父组件(而不是只传 id),父组件据此把该条记录的 starred 字段置为 true 并弹提示。
  4. 把卡片拆成真正的两个文件:新建 src/components/StudentCard.vue,把子组件搬进去(模板写法),App.vue 里 import 进来用。这一步是本课的重点作业,做完后页面行为应和之前完全一致。
  5. 验证只读:在子组件里写一行试图改 props 的代码,把控制台警告截图/抄下来,然后删掉。

按这张操作单完成

步骤 改哪里 运行后应该看到什么
1 换数据 父组件 list 6 条自己的记录。
2 加 props 子组件 props + 模板 关掉开关后卡片上不再显示得分行。
3 加 emit 子组件按钮 + 父组件处理 点收藏后父组件数据里该条 starred 变 true。
4 拆文件 新建 components/StudentCard.vue 行为与拆之前完全一致。
5 验证只读 临时写一行改 props Console 出现 readonly 警告。

最低完成标准:先把这三项做实

检查项 通过条件
props 传值 子组件里没有任何写死的数据,全部来自 props。
emits 通信 至少两个自定义事件生效,且父组件能收到载荷。
单向数据流 子组件代码里找不到任何对父组件数据的直接读写。

在本地写练习记录

  1. 画一张父子通信图:标出哪条线是 props、哪条线是 emit。
  2. 抄下 Missing required prop 的完整报错原文。
  3. 写下拆文件前后的对比感受:哪个更好维护,为什么。

最后交什么

提交 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() 里绑事件要包一层箭头函数,别直接调用。

不要用这些办法“修复”

  1. 不要为了让子组件能改数据,用 defineModel 或直接把数组 provide 下去绕过 props——在数据归属不清的项目里,这类“抄近路”会在后面第 4 阶段引发大量难查的问题。
  2. 不要把所有数据都塞进一个超大的 props 对象(例如传整个 store 进去)。props 应该只传这个组件真正需要的东西。
  3. 不要因为“报 required 警告”就把 required: true 删掉了事,先想清楚这个数据是不是真的必须传。

需要老师帮忙时交这些

子组件的 props 声明、父组件的传值写法、@事件名 的监听写法、Console 里完整的警告或报错原文、你点了什么、期望发生什么、实际发生什么。

老师巡视时只问三句话

  1. 子组件里有没有直接改 props 的地方?(能自查并指出没有)
  2. 这个按钮点下去,数据是谁改的?(能指出父组件里的处理函数)
  3. 如果这个卡片要用在别的页面,你能不能说清它需要哪些 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,容易莫名其妙失效。
组件拆分的第一条好处是什么? 复用。同一段界面只写一次,改一处全生效。

一分钟现场演示指南

  1. 展示复用:说明同一份卡片代码只写了一次,页面却出现很多张。
  2. 展示父子通信:指着三点讲清 —— 数据从父组件进来(props),点击事件从子组件出去(emit),父组件改完数据再回到界面。
  3. 展示纪律:打开子组件源码,指出里面没有 list 这个变量。
  4. 收尾一句:“组件之间不互相改数据,只通过 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 官方文档