大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
组件系统
这一课你将能做到
- 把页面拆分成可复用的组件
- 用
props向子组件传数据 - 用
emits让子组件向父组件发消息 - 用
slots实现内容分发 - 用
provide/inject跨层级传数据
为什么要学
组件是 Vue 的「积木」。一个页面由无数小组件拼成:按钮、卡片、表单、弹窗。不会组件化,代码会越写越乱,改一处崩三处。
人话:组件就像乐高积木。每块积木(组件)有自己的形状(props)和接口(emits),你可以把它们拼成城堡(页面),也能拆下来重复用。
先建立图像
<!-- UserCard.vue -->
<script setup>
defineProps({ name: String, age: Number })
</script>
<template>
<div class="card">
<h3>{{ name }}</h3>
<p>{{ age }} 岁</p>
</div>
</template>
<!-- App.vue -->
<script setup>
import UserCard from './components/UserCard.vue'
</script>
<template>
<UserCard name="张三" :age="25" />
<UserCard name="李四" :age="30" />
</template>
跟着做
步骤 1:创建并使用组件
在 src/components/ 下新建 HelloWorld.vue:
<script setup>
const props = defineProps({ msg: String })
</script>
<template>
<p>{{ props.msg }}</p>
</template>
在 App.vue 中引入使用:
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld msg="你好" />
</template>
步骤 2:props 传数据
<script setup>
// 数组语法(简单)
defineProps(['title', 'count'])
// 对象语法(推荐,可校验)
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
items: { type: Array, default: () => [] }
})
</script>
props 是单向数据流:父传子,子不能改。
步骤 3:emits 子传父
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['add', 'remove'])
function handleClick() {
emit('add', 1)
}
</script>
<template>
<button @click="handleClick">通知父组件</button>
</template>
<!-- Parent.vue -->
<script setup>
function onAdd(n) { console.log('收到', n) }
</script>
<template>
<Child @add="onAdd" />
</template>
步骤 4:slots 插槽
<!-- Card.vue -->
<template>
<div class="card">
<div class="header"><slot name="header">默认标题</slot></div>
<div class="body"><slot>默认内容</slot></div>
</div>
</template>
<!-- App.vue -->
<Card>
<template #header><h2>自定义标题</h2></template>
<template #default><p>自定义内容</p></template>
</Card>
步骤 5:provide / inject 跨层级
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- 任意后代组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
</script>
适合「主题、用户信息」这类全局状态,避免 props 逐层透传。
讲清楚要点
- props 是只读的:子组件不能修改 props,要改就发事件让父组件改。
- emits 是自定义事件:子组件通过
emit('事件名', 数据)向父组件通信。 - slot 是内容分发:父组件决定子组件的「内容」,子组件决定「骨架」。
- provide/inject 是依赖注入:跨多层传递数据,但慎用,会让组件耦合变高。
- 组件命名:PascalCase(
UserCard),模板里可用<user-card>或<UserCard>。
你可能会懵的地方
-
props 和 data 有什么区别? props 是外部传入的,data(或 ref)是组件内部自己的。props 由父组件控制,data 由自己控制。
-
什么时候用 slot,什么时候用 props? 传「数据」用 props,传「HTML 结构」用 slot。比如弹窗组件:标题文字用 props,弹窗内容用 slot。
-
provide/inject 和 Vuex/Pinia 怎么选? provide/inject 适合组件树内的局部共享;全局状态(用户信息、购物车)用 Pinia。
-
组件名必须和文件名一致吗? 不必须,但强烈建议一致,方便调试和搜索。
-
兄弟组件怎么通信? 通过共同父组件(状态提升),或用 Pinia,不推荐用事件总线。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 子组件直接修改 props | Vue 警告,数据流混乱 | 用 emit 通知父组件修改 |
模板里写 <hello-world> 但组件没注册 | 标签被当原生 HTML | 在 <script setup> 中 import |
props 类型写错:type: number | 应该首字母大写 | type: Number |
用 v-model 传对象,子组件直接改属性 | 违反单向数据流 | 用 emit('update:xxx') 或深拷贝 |
| 滥用 provide/inject 传所有数据 | 组件耦合高,难追踪 | 只传真正全局的数据 |
卡点急救
- 组件不渲染:检查 import 路径是否正确,文件名大小写是否匹配。
- props 拿不到:检查父组件传值是否加冒号,
<Child count="1">传的是字符串,<Child :count="1">传的是数字。 - emit 没反应:检查父组件监听的事件名是否一致,
emit('add')对应@add="handler"。
面试常问
-
组件通信有哪些方式? props / emits、provide / inject、slot、Pinia、路由参数。优先级:能 props 不 provide,能组件内不全局。
-
什么是单向数据流? 数据从父到子单向流动,子组件不能修改父组件的数据,保证数据可追踪。
-
slot 和 props 的本质区别? props 传数据,slot 传模板结构。slot 让组件更灵活,props 让组件更可控。
自测三问
- props 和 emits 分别解决什么方向的通信?
- 什么场景下应该使用 slot 而不是 props?
- provide/inject 和 Pinia 的适用边界是什么?
小结一句话
组件化是前端工程化的核心,props 向下传数据,emits 向上发事件,slot 分发内容,provide/inject 跨层级,掌握这四种通信方式就能搭建任意复杂度的页面。
延伸
- Vue 官方文档 - 组件基础
- 下一课:响应式进阶——ref、reactive、computed、watch。