大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
组件与 Props
这一课你将能做到
- 拆分父子组件,用 props 向下传数、用
emit向上发事件 - 使用具名/默认 slot 做布局插槽
- 对照 React:props 只读、子通知识父,思想一致、API 不同
为什么要学
单文件越写越长就要拆。拆分后:父管数据与流程,子管展示与局部交互。
本站 React 课同样强调单向数据流;Vue 用 props + emit + slot 表达。
先建立图像
父 ──props──▶ 子
父 ◀──emit─── 子
父在模板里写子标签时,子的 slot 位置被父填内容
| 通道 | 方向 | 人话 |
|---|---|---|
| props | 父→子 | 参数,子只读 |
| emit | 子→父 | 通知父「发生了某事」 |
| slot | 父→子的坑位 | 把模板片段塞进子 |
跟着做
步骤 1:子组件声明 props
<!-- TrackCard.vue -->
<script setup>
const props = defineProps({
title: { type: String, required: true },
level: { type: String, default: "beginner" },
});
</script>
<template>
<article>
<h2>{{ props.title }}</h2>
<p>难度:{{ level }}</p>
</article>
</template>
模板里可直接用 level(setup 已暴露)。defineProps 是编译期宏,无需 import。
步骤 2:父传入
<script setup>
import TrackCard from "./TrackCard.vue";
</script>
<template>
<TrackCard title="Vue 3 核心" level="intermediate" />
</template>
步骤 3:子 emit,父处理
<!-- CounterBtn.vue -->
<script setup>
const emit = defineEmits(["inc"]);
</script>
<template>
<button type="button" @click="emit('inc', 1)">加一</button>
</template>
<script setup>
import { ref } from "vue";
import CounterBtn from "./CounterBtn.vue";
const total = ref(0);
</script>
<template>
<p>{{ total }}</p>
<CounterBtn @inc="(n) => (total += n)" />
</template>
注意:模板里 total 是 ref 自动解包,total += n 可行;在纯 script 回调里记得 .value。
步骤 4:默认 slot
<!-- Panel.vue -->
<template>
<section class="panel">
<slot>默认内容</slot>
</section>
</template>
<Panel>
<p>侧栏说明</p>
</Panel>
步骤 5:具名 slot(布局)
<!-- Layout.vue -->
<template>
<header><slot name="header" /></header>
<main><slot /></main>
</template>
<Layout>
<template #header><h1>CodeRoadMap</h1></template>
<p>正文</p>
</Layout>
步骤 6:对照 React
| Vue | React |
|---|---|
defineProps | 函数参数 props |
emit('x') + @x | onX 回调 props |
<slot> | children / 渲染 props |
写三句笔记完成迁移。
讲清楚要点
- props 单向:子不要直接改 prop;要改就 emit 让父改源。
- 显式 emits 利于文档与校验,避免随意
$emit拼写错误。 - 对象/数组 prop 仍是引用:子改内部字段会波及父——约定上仍应当只读,或解构到本地副本。
- slot 解决「父子都要定制中间 UI」:比层层 props 传 VNode 更符合模板思维。
- 组件名与文件名:一致更好找。
- 本站卡片列表:React 里 map 出卡片;Vue 同样 map/
v-for出子组件。
你可能会懵的地方
-
defineProps返回值要不要接?
模板只用 prop 名时可不接;在 script 里用再接props。 -
camelCase vs kebab-case?
DOM 模板里常用 kebab;SFC 模板两者多可。保持团队一致。 -
透传 attributes?
非 props 的属性默认落到根元素;进阶再挖inheritAttrs。 -
provide/inject?
跨多层传递;下一课之后、Context 对照时再系统用。本课先 props。 -
循环引用组件?
可能需异步组件;入门先避免环形依赖。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 子改 prop | 单向流破坏、难追 | emit 通知父 |
| 事件名拼错 | 父听不到 | defineEmits + 一致命名 |
| 巨型单文件 | 难测 | 按 UI 区块拆 |
| slot 与 props 重复造同一数据 | API 混乱 | 数据 props,结构 slot |
| 无 key 的列表组件 | 状态错乱 | v-for :key |
| 把 Vue 组件塞进 React 树硬混 | 运行时灾难 | 分项目练习 |
自测三问
- 为什么说 props 是只读的?
- emit 相对「子直接改父的对象字段」好在哪?
- 默认 slot 与具名 slot 各适合什么?
小结一句话
组件用 props 向下、emit 向上、slot 填坑;保持单向数据流,拆分才可控。
动手加深
把本课「跟着做」再跑一遍,但这次故意先写出错法表里的一种写法,观察报错或错误现象 30 秒,再改回正解。
用自己的话补三条笔记:① 我最容易踩的坑;② 成功时屏幕/结果长什么样;③ 本课与上一课的衔接点。写不出来就重做步骤,不要只标完成。