Vue 3 组件完全指南
本文档从零开始讲解 Vue 3 的组件:什么是组件、如何定义和使用、如何通过 props 传数据、如何通过事件与父组件通信、以及插槽、v-model、多根节点等,配有大量示例,适合新手系统学习。
一、什么是组件?
1.1 生活中的类比
可以把组件想象成积木块:每块积木有自己的形状和功能,可以反复使用、随意组合。
在 Vue 里,组件就是一块“可复用的界面 + 逻辑”的单元:例如一个按钮、一张卡片、一个表单输入框。
你把多个组件拼在一起,就得到一整页。
1.2 为什么要用组件?
- 复用:同一个“按钮样式 + 点击逻辑”在多处使用,只写一次组件即可。
- 拆分:页面拆成多个小组件,结构清晰,便于维护。
- 协作:不同人负责不同组件,减少冲突。
1.3 组件长什么样?
在 Vue 3 中,一个组件通常对应一个 .vue 文件,称为单文件组件(SFC)。
文件里一般包含三块:
<template>:HTML 结构(可写 Vue 模板语法)。<script>:逻辑(数据、方法、生命周期等)。<style>:样式(可加scoped只影响当前组件)。
<!-- HelloWorld.vue -->
<template>
<div>
<p>你好,{{ name }}!</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('Vue 3')
</script>
<style scoped>
p {
color: blue;
}
</style>
上面就是一个最简单的组件:有结构、有数据、有样式。
下面从“如何使用组件”开始,再讲“如何通过 props 和事件与外界通信”。
二、如何使用组件?
使用组件分两步:引入、在模板里当标签用。
2.1 在别的组件里使用(局部注册)
- 用
import把组件文件引进来。 - 在
<script setup>里,import 进来的组件会自动在当前组件的模板里可用,不用再写 components 注册(这是<script setup>的便利之处)。 - 在
<template>里用标签的形式写组件,标签名用帕斯卡命名(PascalCase)或短横线(kebab-case)都可以,Vue 会识别。
<!-- App.vue -->
<template>
<div>
<h1>我的应用</h1>
<HelloWorld />
<!-- 也可写成 <hello-world /> -->
</div>
</template>
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
注意:
- 路径
./components/HelloWorld.vue要按你实际文件位置改。 - 组件名推荐用 PascalCase(如
HelloWorld),在模板里可以写成<HelloWorld />或<hello-world />。
2.2 全局注册(可选)
若希望整个应用很多地方都用同一个组件,可以在入口文件里全局注册,这样就不用在每个页面里 import 了。
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'
const app = createApp(App)
app.component('MyButton', MyButton) // 全局注册,任意模板里可直接用 <MyButton />
app.mount('#app')
一般建议:只有真的到处都要用的组件才全局注册,其它用局部 import,更清晰。
三、Props:父组件向子组件传数据
父组件把数据通过属性的形式传给子组件,子组件用 props 接收。
这样同一个组件在不同地方可以显示不同内容(例如同一个“卡片”组件,显示不同标题和正文)。
3.1 子组件:声明 props
在子组件的 <script setup> 里用 defineProps 声明“我接收哪些属性、类型是什么、是否必填、默认值是什么”。
defineProps 是编译器宏,不需要 import,直接写即可。
只写属性名(简单写法):
<!-- Card.vue -->
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</template>
<script setup>
defineProps(['title', 'content'])
</script>
<style scoped>
.card {
border: 1px solid #ddd;
padding: 16px;
border-radius: 8px;
}
</style>
带类型和默认值(推荐):
<script setup>
defineProps({
title: {
type: String,
required: true
},
content: {
type: String,
default: ''
},
count: {
type: Number,
default: 0
}
})
</script>
这样父组件传过来的 title、content、count 在模板里可以直接用(只读,不要改)。
3.2 父组件:传 props
在父组件里,把子组件当标签用,通过属性把数据传进去。
属性名用 kebab-case(如 user-name)或 camelCase(如 userName)都可以,Vue 会对应到 props 里的 camelCase 名。
<!-- App.vue -->
<template>
<div>
<Card title="第一张卡片" content="这是内容" />
<Card :title="dynamicTitle" :content="dynamicContent" />
<Card title="只有标题" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Card from './components/Card.vue'
const dynamicTitle = ref('动态标题')
const dynamicContent = ref('动态内容')
</script>
- 静态传字符串:
title="第一张卡片"。 - 动态传变量:用
:title="dynamicTitle"(:即v-bind,后面是表达式)。 - 没传的 prop 会使用子组件里定义的 default。
3.3 单向数据流:不要改 props
Props 是只读的。 父组件传过来的值,子组件不要直接修改(如 props.title = 'x')。
若子组件要根据 prop 做计算或内部状态,应该:
- 用 计算属性 基于 prop 派生新值;或
- 用 ref/reactive 在组件内拷贝一份,改自己的拷贝。
<script setup>
const props = defineProps({ initialCount: { type: Number, default: 0 } })
// 若需要内部可变的“计数”,用 ref 拷贝
const count = ref(props.initialCount)
</script>
四、事件:子组件向父组件“汇报”
子组件通过 触发事件 把“发生了某件事”告诉父组件,父组件监听事件并执行自己的逻辑。
这样点击子组件里的按钮,可以让父组件去请求接口、改状态等。
4.1 子组件:定义并触发事件
在 <script setup> 里用 defineEmits 声明“我会触发哪些事件”,然后在需要的地方调用 emit('事件名', 参数)。
<!-- MyButton.vue -->
<template>
<button @click="handleClick">点击我</button>
</template>
<script setup>
const emit = defineEmits(['click'])
function handleClick() {
emit('click')
}
</script>
若需要传参给父组件,把参数放在 emit 的第二个及之后的参数里:
<script setup>
const emit = defineEmits(['submit'])
function submit() {
emit('submit', { name: '小明', age: 18 })
}
</script>
4.2 父组件:监听事件
在父组件里,给子组件标签上写 @事件名="处理函数",和监听原生 DOM 事件一样。
<!-- App.vue -->
<template>
<div>
<MyButton @click="onClick" />
<MyForm @submit="onSubmit" />
</div>
</template>
<script setup>
import MyButton from './components/MyButton.vue'
import MyForm from './components/MyForm.vue'
function onClick() {
console.log('按钮被点了')
}
function onSubmit(data) {
console.log('提交的数据', data)
}
</script>
这样就把“子组件发生的事”和“父组件的逻辑”连起来了。
五、组件上的 v-model(双向绑定)
我们不仅希望父传子(props),还希望子组件内部变化能同步回父组件(例如输入框的值)。
除了用“prop + 事件”自己实现,还可以用 v-model,让子组件支持 v-model="父组件的变量"。
5.1 子组件:接收 modelValue 并触发 update:modelValue
在 Vue 3 中,组件上的 v-model 默认对应:
- 名为
modelValue的 prop; - 名为
update:modelValue的事件。
子组件内部:用 props 接收 modelValue,在值变化时 emit('update:modelValue', 新值)。
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
</script>
5.2 父组件:直接 v-model
<template>
<CustomInput v-model="username" />
<p>你输入了:{{ username }}</p>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './components/CustomInput.vue'
const username = ref('')
</script>
这样父组件的 username 和子组件输入框就实现了双向绑定。
若需要多个“双向绑定”(例如一个组件同时绑定“用户名”和“密码”),可以用多个 v-model,见下一小节。
5.3 多个 v-model:指定名字
给 v-model 加“名字”:v-model:名字=”变量”。
子组件则接收 名字 这个 prop,并触发 update:名字。
<!-- UserForm.vue -->
<template>
<div>
<input :value="username" @input="emit('update:username', $event.target.value)" />
<input :value="age" @input="emit('update:age', Number($event.target.value))" />
</div>
</template>
<script setup>
defineProps({
username: String,
age: Number
})
const emit = defineEmits(['update:username', 'update:age'])
</script>
<!-- 父组件 -->
<template>
<UserForm v-model:username="user.name" v-model:age="user.age" />
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({ name: '', age: 0 })
</script>
六、插槽:父组件往子组件里“塞内容”
子组件可以在模板里留插槽(<slot>),父组件在使用子组件时,把一段内容(HTML 或其它组件)塞进插槽里。
这样同一个“外壳”组件可以显示不同内容。
插槽分为:默认插槽、具名插槽、作用域插槽。这里只做简要说明,详细用法见《Vue3插槽.md》。
子组件留一个洞:
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-body">
<slot></slot>
</div>
</div>
</template>
父组件塞内容:
<Card>
<p>任意内容、按钮、图片都可以</p>
</Card>
七、组件的根节点:单根与多根(Vue 3 支持多根)
- Vue 2:
<template>里必须有一个根元素。 - Vue 3:
<template>里可以有多个根元素(多个根节点),称为 Fragment。
<!-- Vue 3 允许这样写 -->
<template>
<header>头部</header>
<main>主体</main>
<footer>底部</footer>
</template>
多根时,属性、事件只能写在某一个根上,不会自动继承到所有根;需要时请用一个包裹用的根(如 <div>)或分别写在每个根上。
八、组件名与使用方式
- 定义时:文件名和组件名推荐用 PascalCase,如
UserCard.vue。 - 模板里使用:
- PascalCase:
<UserCard /> - kebab-case:
<user-card />
Vue 会把两者都识别为同一个组件。
- PascalCase:
- 在 DOM 里(非 Vue 模板,如直接写 HTML),只能写小写或短横线,所以最终渲染出来的标签会是小写(如
<user-card>)。