[toc]
| 单词 | 词性 | 音标 | 含义 |
|---|---|---|---|
| component | n. | /kəmˈpəʊnənt/ | 组件,一块可复用的“积木”,一个组件 = 一个 .vue 文件 |
| slot | n. | /slɒt/ | 插槽,子组件里留的“空位”,父组件往里塞内容 |
| props | n. | /prɒps/ | 属性(参数),父组件传给子组件的数据,只读 |
| emit | v. | /iˈmɪt/ | 触发(事件),子组件用 emit 把数据上报给父组件 |
| reusable | adj. | /riˈjuːzəbl/ | 可复用的,组件化的核心价值 |
| scoped | adj. | /ˈskəʊpt/ | 作用域受限的,样式只对当前组件生效 |
| communication | n. | /kəˌmjuːnɪˈkeɪʃn/ | 通信,组件之间传递数据的各种方式 |
| defineProps | - | /dɪˈfaɪn prɒps/ | 编译宏,声明这个组件接收哪些 props |
Lecture08-组件:定义、插槽、props、事件
1、本节讲什么
学会"把页面拆成积木":定义组件、用组件拼成页面、用插槽定制内容、用 props 把数据传进子组件、用 emit 事件把数据从子组件传回父组件。这是 Vue 组件复用的全部地基,也是后面做后台管理系统的核心。
说明:本节合并了"组件基础 + 组件通信",通过 4 个实验由浅入深把组件一次讲透。
2、概念速览
什么是组件:把一块页面(结构 <template> + 样式 <style> + 行为 <script>)打包成一个可复用的"积木"。一个组件 = 一个 .vue 文件。

组件的作用:① 复用——一处写好、到处用;② 分工——页面拆成小积木,各管各的、好维护;③ 独立——scoped 让样式只对自己生效。

Vue组件的三个组成部分:
- 【结构】template 标签:编写 HTML 代码,也可以引入其它组件
- 【行为】script 标签:编写 JavaScript、TypeScript 代码
- 【表现】style 标签:编写 CSS 代码
Vue组件文件的扩展名:*.vue
组件的三种数据关系(本节学前两种,Pinia 在第 18 节):
- 纵向:父 → 子用 props(数据下传);子 → 父用 emit 事件(消息上报)。
- 横向:兄弟组件之间没有"直连线",要么借父组件中转,要么上 Pinia(详见本节常见疑问 + 配图)。
- 全局共享:Pinia。
正确 vs 误区:组件不是"一个文件"这么简单,核心是"独立、可复用、只干一件事";一个页面 = 多个组件组装,而不是一个大文件堆到底。
3、先看效果
做一个最简单的"页面 = 组件拼出来"的演示——两个小组件拼成一个带顶部栏和按钮的页面(不涉及任何数据传递,先看组件怎么组合):
src/components/Header.vue(子组件:顶部栏)
<template>
<header class="top">后台管理系统</header>
</template>
<style scoped>
.top { height: 50px; background: #42b883; color: #fff; line-height: 50px; text-align: center; }
</style>
src/components/MyButton.vue(子组件:一个按钮)
<template>
<button class="btn">默认按钮</button>
</template>
<style scoped>
.btn { padding: 8px 16px; cursor: pointer; } /* 父组件管"布局位置",子组件管"自己长什么样" */
</style>
App.vue(父组件:把两个子组件组装成页面)
<script setup lang="ts">
import Header from './components/Header.vue'
import MyButton from './components/MyButton.vue'
</script>
<template>
<Header />
<div class="layout"><MyButton /></div>
</template>
<style scoped>
.layout { padding: 20px; } /* 父组件管"布局位置",子组件管"自己长什么样" */
</style>
运行后:顶部一条栏 + 中间一个按钮,页面就是由多个小组件拼起来的。
4、照着做:4 个实验
实验 1 · 创建并使用组件(不传数据,观察组合)
步骤 1:创建子组件。新建 src/components/Header.vue 和 MyButton.vue(如上)。
步骤 2:在父组件引入子组件。在 App.vue 里 import,并在模板里用 <Header />、<MyButton /> 摆放。
步骤 3:编写必要的样式,控制各组件位置。
- 子组件用
<style scoped>管自己内部的样式(高度、颜色)。 - 父组件用布局(如
flex、padding)管组件在页面里的位置。
体会:子组件"管自己长什么样",父组件"管怎么摆"——职责分离。
实验 2 · 父组件通过插槽定制内容
步骤 1:在子组件中编写 slot 标签(留一个"空位"):
<!-- src/components/MyButton.vue -->
<template>
<button class="btn"><slot /></button>
</template>
<slot /> 就是插槽——一个空位,等外面往里塞内容。
步骤 2:在父组件往插槽位置传入内容:
<MyButton>登录</MyButton>
<MyButton>注册</MyButton>
<MyButton> 和 </MyButton> 之间写的内容,会显示在子组件的 <slot /> 位置。同一个按钮组件,通过插槽就能显示不同文字——组件留白、外面定制。
实验 3 · 父组件通过 props 把数据传入子组件
步骤 1:在子组件创建 props 对象(先写 interface,好理解;再用简化写法):
<!-- src/components/MyCard.vue -->
<script setup lang="ts">
// 写法一:interface 声明"这个组件收哪些参数",清晰好懂
interface Props {
name: string
age: number
}
const props = defineProps<Props>()
// 写法二:简化写法(内联),和写法一完全等价
// const props = defineProps<{ name: string; age: number }>()
</script>
步骤 2:把 props 的各个属性渲染到 HTML:
<template>
<p>{{ props.name }} · {{ props.age }} 岁</p>
</template>
步骤 3:在父组件给各个 props 属性赋值(注意加不加冒号):
<MyCard name="小红" :age="22" />
加不加冒号的区别:
name="小红"不带冒号,传的是字符串字面量;:age="22"带冒号(v-bind简写),传的是数字/变量/表达式。要传数字、布尔、数组、变量,必须用:xxx。
实验 4 · 子组件通过 emit 事件把数据向上传给父组件
步骤 1:在子组件定义 emit 事件:
<!-- src/components/MyCounter.vue -->
<script setup lang="ts">
// 声明:这个组件能触发 add 事件,带一个 number 参数
const emit = defineEmits<{ add: [num: number] }>()
</script>
<template>
<button @click="emit('add', 1)">点我 +1</button>
</template>

步骤 2:在子组件调用 emit 并传入数据——上面 @click="emit('add', 1)" 就是触发 add 事件、把 1 作为数据传出去。
步骤 3:在父组件监听子组件的 emit 事件并接收数据:
<script setup lang="ts">
import MyCounter from './components/MyCounter.vue'
import { ref } from 'vue'
const total = ref(0)
function onAdd(n: number) { total.value += n }
</script>
<template>
<MyCounter @add="onAdd" />
<p>点击数:{{ total }}</p> <!-- 步骤4:把收到的数据渲染到 HTML -->
</template>
步骤 4:父组件把从事件接收到的数据渲染到 HTML——上面 <p>点击数:{{ total }}</p> 就是。
方向记忆:props 是父发(
:传下去)子收;emit 是子发(emit('add', n))父收(@add)。 值永远从子组件那边emit带出来。
5、常见疑问
-
Q:子组件之间如何横向传递数据? A:Vue 里兄弟组件没有"直连线",只有三种方式(见下图):
- ① 借父组件中转(最常用):A 发事件给父 W,父更新自己的数据,再通过 props 传给 B——本质是"两条纵向"拼出来的。
- ② Pinia 全局共享(第 18 节):A、B 都去读写同一个全局仓库,绕开父子关系,跨组件、数据多时最省事。
- ③ 事件总线(不推荐):让兄弟"直连",但数据流隐蔽、难追踪,是屎山诱因。
- ④另外还有一种方式是在路由跳转过程中携带参数,但通常认为这种情况属于『页面』之间传参
- ⑤总结:每一种数据传递的技术,都对应一个特定的场景;特定场景使用恰当的技术来传递数据,有利于优化项目的代码结构
-
Q:props 和 Java 的方法参数像吗? A:很像——父组件"传参",子组件声明"接收哪些参数"。好处是组件可复用、可参数化。
-
Q:
name="小红"和:age="22"有什么区别? A:不带冒号传字符串字面量;带冒号(v-bind简写)传数字/变量/表达式。传数字、布尔、数组必须用:xxx。 -
Q:子组件能改 props 吗? A:不能。props 是父传子、只读。子组件要改,就 emit 事件让父组件改——这叫"单向数据流",保证数据流向清晰、好排查。

-
Q:为什么 props 不能被 Pinia 完全取代? A:props 是"组件复用地基"——让组件可参数化(同一个卡片,传不同数据显示不同内容),这是组件复用的本质。Pinia 是"全局共享仓库",管的是"多个不相干组件共用的数据"。局部传参用 props,全局共享才用 Pinia,全塞 Pinia 反而更乱(详见第 18 节对比)。
-
Q:
slot到底是什么? A:给组件留的一个"空位"。外面往空位塞什么,就显示什么。让组件能"留白"、更通用,不用每次写死内容。 -
Q:如果 Vue 插件提供的组件骨架代码片段快捷方式无效或不稳定,怎么解决? A:Ctrl + Shift + P
使用下面代码片段覆盖原内容:{ "Vue3 TS Component": { "prefix": "vue3", "body": [ "<script setup lang=\"ts\">", "", "</script>", "", "<template>", " <div></div>", "</template>", "", "<style scoped>", "", "</style>" ], "description": "Vue3 setup ts component" } }
6、规范要点
- 组件职责单一:一个组件只做一件事,复杂了就拆;页面 = 组件组装。
- 组件命名大驼峰(
MyCard.vue),文件名和组件名一致,放src/components/。 - props 只读:子组件不改 props,靠 emit 通知父组件——这是防屎山的核心之一。
- 组件对外接口明确:
defineProps负责接收数据、defineEmits负责发送数据,不能乱。 - 传复杂数据(对象/数组)用
:xxx,并配合interface标注类型。 - 横向传数据优先借父组件中转或 Pinia,别用事件总线。
- 通用小零件(按钮、卡片、表格)先想能不能用现成的(Element Plus,第 11 节),别急着自己造。
- ElementPlus 提供具体控件
- 组件根据业务的需求创建页面布局单元
7、本节验证
- 会拆组件:把页面拆成若干
.vue积木再组装 ✅ - 会用
<slot />让父组件定制子组件的内容 ✅ - 会用
defineProps接收数据,并分清name="小红"与:age="22"✅ - 会用
defineEmits+emit('add', n)把数据传回父组件 ✅ - 知道 props 只读,兄弟组件横向传数据借父组件或 Pinia ✅