认识单文件组件
Q: 什么是 Vue 的单文件组件?它有什么优势?
A: 在 Vue 中支持单文件组件(Single File Component,SFC),也就是一个文件对应一个组件,文件以 .vue 作为后缀。一个 .vue 文件包含了完整的三件套——结构(template)、逻辑(script)、样式(style),真正做到了高内聚。
<template>
<button @click="count++">Count is: {{ count }}</button>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<style scoped>
button {
padding: 15px;
}
</style>
script setup 语法糖
在 Vue3 初期,<script> 中需要手动 return 模板中要用到的数据和方法:
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
function add() {
count.value++;
}
return {
count, // 必须手动返回
add,
};
},
};
从 Vue 3.2 开始,推出了 <script setup> 语法糖。所有在 <script setup> 中定义的顶层变量和函数,都会自动暴露给模板使用,无需手动 return,大幅减少了样板代码。
此外,<script setup> 还有两个额外优势:
- 更好的类型推断(TypeScript 支持更友好)
- 支持顶级 await(直接在 setup 顶层使用 await,无需包裹在 async 函数中)
scoped 局部样式
<style scoped> 中的 scoped 关键字表示样式仅对当前组件生效——Vue 会通过属性选择器的方式自动为组件内的元素添加唯一标识,确保样式不会泄露到其他组件。
如果不写 scoped,那么样式就是全局生效的,可能污染其他组件。因此在实际开发中,强烈建议始终为组件样式加上 scoped。
组件注册
Q: 全局注册和局部注册有什么区别?应该如何选择?
A: 组件注册分为两种方式:
全局注册
使用 Vue 应用实例的 .component() 方法注册,注册后所有组件都可以直接使用,无需再单独导入:
import { createApp } from 'vue';
const app = createApp({});
app.component(
'MyComponent', // 注册的名字
{
/* 组件的实现 */
},
);
更常见的写法是导入 .vue 文件后注册:
import MyComponent from './App.vue';
app.component('MyComponent', MyComponent);
.component() 方法支持链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC);
局部注册
局部注册就是在使用组件的那个文件中导入它,然后注册。使用选项式 API 时需要通过 components 配置项声明:
<template>
<button @click="add">Count is: {{ count }}</button>
<TestCom />
</template>
<script>
import { ref } from 'vue';
import TestCom from './components/TestCom.vue';
export default {
components: {
TestCom, // 局部注册
},
setup() {
const count = ref(0);
function add() { count.value++; }
return { count, add };
},
};
</script>
而使用 <script setup> 语法糖时,导入即注册,无需额外声明:
<template>
<button @click="add">Count is: {{ count }}</button>
<TestCom />
</template>
<script setup>
import { ref } from 'vue';
import TestCom from './components/TestCom.vue';
const count = ref(0);
function add() { count.value++; }
</script>
如何选择?
实际开发中,推荐局部注册。理由:
- 全局注册的组件无法被 tree-shaking(打包工具无法移除未使用的全局组件)。
- 在大型项目中,全局注册无法直观地看出组件之间的依赖关系。
组件命名规范
推荐使用大驼峰(PascalCase)命名组件,如 UserCard、TodoList。不过需要注意:大驼峰在 DOM 内模板(如直接在 HTML 文件中使用)中无法使用,此时需改为短横线命名。
Props:父组件向子组件传递数据
Q: Props 是什么?如何在子组件中接收父组件的数据?
A: 组件在应用层面有三个核心知识点——Props、自定义事件、插槽。Props 解决的是父组件向子组件传递数据的问题。
快速入门
首先定义一个 UserCard.vue 子组件:
<template>
<div class="user-card">
<img :src="user.avatarUrl" alt="用户头像" class="avatar" />
<div class="user-info">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</div>
</template>
<script setup>
// defineProps 是一个宏,用于声明组件接收哪些 props
const user = defineProps({
name: String,
email: String,
avatarUrl: String,
});
</script>
defineProps 是一个编译宏,在代码实际执行之前会被替换。它不需要从 vue 中导入,直接使用即可。
父组件 App.vue 中使用该组件:
<template>
<div class="app-container">
<UserCard
name="张三"
email="123@gamil.com"
avatar-url="src/assets/yinshi.jpg"
/>
<UserCard
name="莉丝"
email="456@gamil.com"
avatar-url="src/assets/jinzhu.jpeg"
/>
</div>
</template>
<script setup>
import UserCard from './components/UserCard.vue';
</script>
Props 使用细节
1. 命名风格
子组件内部声明 props 时,推荐使用驼峰命名法:
defineProps({
greetingMessage: String,
});
<span>{{ greetingMessage }}</span>
而父组件传递时,推荐使用更贴近 HTML 风格的短横线命名:
<MyComponent greeting-message="hello" />
Vue 会自动完成两种命名风格的转换。
2. 动态 Props
上面示例中传递的都是静态字符串。如果将 prop 与父组件自身的状态绑定,就是动态 Props:
<template>
<div class="app-container">
<UserCard :user="user" />
<div class="input-group">
<input type="text" placeholder="请输入新的名字" v-model="newName" />
<button @click="changeName">确定修改</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import UserCard from './components/UserCard.vue';
const user = ref({
name: '张三',
email: '123@gamil.com',
avatarUrl: 'src/assets/yinshi.jpg',
});
const newName = ref('');
function changeName() {
if (newName.value.trim()) {
user.value.name = newName.value;
}
}
</script>
重要提示:如果想要向组件传递非字符串类型的值(如 number、boolean、array、object),必须使用动态 Props(v-bind 或 : 前缀),否则组件内部拿到的永远是字符串。
3. 单向数据流
Props 的数据流向是单向的——从父组件到子组件。这意味着子组件中不应该修改父组件传递下来的 Props。
如果强行修改,Vue 会在控制台抛出警告:
const props = defineProps(['foo']);
// ❌ 警告!prop 是只读的!
props.foo = 'bar';
有些时候你可能需要在子组件中「基于 props 做一份自己的数据」。这种情况下,应该在子组件内部新建一个响应式数据,用 props 的值初始化:
import { ref } from 'vue';
const prop = defineProps(['user', 'age']);
// ✅ 正确:在子组件中创建新的响应式数据
const age = ref(prop.age);
如果需要对 props 值进行二次计算,同样应该在子组件内部使用计算属性:
const props = defineProps(['size']);
// 基于 props 做二次计算,不修改原值
const normalizedSize = computed(() => props.size.trim().toLowerCase());
Props 校验
子组件声明 Props 时,可以携带校验规则。如果父组件传递的值不符合要求,Vue 会在开发阶段的控制台给出警告信息。
完整的校验配置:
defineProps({
// 基础类型检查
// (null 和 undefined 会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true,
},
// Number 类型的默认值
propD: {
type: Number,
default: 100,
},
// 对象类型的默认值(必须从工厂函数返回)
propE: {
type: Object,
default(rawProps) {
return { message: 'hello' };
},
},
// 自定义类型校验函数(3.4+ 第二个参数为完整的 props)
propF: {
validator(value, props) {
return ['success', 'warning', 'danger'].includes(value);
},
},
// 函数类型的默认值
propG: {
type: Function,
default() {
return 'Default function';
},
},
});
一个实际的自定义校验示例——确保 user 对象包含必需的字段:
defineProps({
user: {
type: Object,
required: true,
validator: (value) => {
return value.name && value.email && value.avatarUrl;
},
},
age: {
type: [Number, String],
default: 18,
},
});
最佳实践:始终为 Props 添加类型声明和必要的校验规则。这不仅是给自己看,更是给使用你组件的同事一个清晰的使用说明。
自定义事件:子组件向父组件通信
Q: 子组件如何向父组件传递数据?为什么需要自定义事件?
A: 自定义事件的核心思想是子组件传递数据给父组件。结合 Props 的「父传子」和自定义事件的「子传父」,就形成了完整的父子组件通信闭环。
另外,由于 Props 是单向数据流(子组件不能修改),当子组件需要通知父组件「该更新数据了」时,就可以通过自定义事件来实现。
快速上手:评分组件
下面是一个经典的评分组件示例:
<template>
<div class="rating-container">
<span v-for="star in 5" :key="star" class="star" @click="setStar(star)">
{{ rating >= star ? '★' : '☆' }}
</span>
</div>
</template>
<script setup>
import { ref } from 'vue';
const rating = ref(0); // 当前评分
// 使用 defineEmits 声明要触发的事件
const emits = defineEmits(['update-rating']);
function setStar(newStar) {
rating.value = newStar;
// 触发父组件绑定的 update-rating 事件,并传递评分值
emits('update-rating', rating.value);
}
</script>
<style scoped>
.rating-container {
display: flex;
font-size: 24px;
cursor: pointer;
}
.star {
margin-right: 5px;
color: gold;
}
.star:hover {
color: orange;
}
</style>
父组件使用评分组件并处理自定义事件:
<template>
<div class="app-container">
<h1>请对本次服务评分:</h1>
<Rating @update-rating="handleRating" />
<p v-if="rating > 0">你当前的评价为 {{ rating }} 颗星</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Rating from './components/Rating.vue';
const rating = ref(0);
function handleRating(newRating) {
rating.value = newRating; // 父组件接收并存储评分
}
</script>
整个流程可以总结为:子组件通过 defineEmits 声明事件 -> 调用 emits() 触发事件并传参 -> 父组件通过 @事件名 绑定处理函数 -> 处理函数接收子组件传来的数据。
模板中使用 $emit
在组件的模板中,也可以直接使用 $emit 触发自定义事件,无需在 script 中声明:
<span
v-for="star in 5"
:key="star"
class="star"
@click="$emit('update-rating', star)"
>
{{ rating >= star ? '★' : '☆' }}
</span>
这种方式适合逻辑非常简单的场景,省去了在 script 中定义 setStar 函数的步骤。
事件校验
和 Props 类似,自定义事件也支持校验——主要用于校验子组件传递给父组件的参数是否符合预期。
要添加校验,需要将 defineEmits 书写为对象形式,对象的键是事件名,值是校验函数:
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true;
} else {
console.warn('Invalid submit event payload!');
return false;
}
},
});
function submitForm(email, password) {
emit('submit', { email, password });
}
</script>
校验函数接收到的是 emit 触发时传递的参数,需要返回一个布尔值表明是否通过校验。
应用到评分组件中——限制评分必须在 1-5 之间:
const emits = defineEmits({
'update-rating': (value) => {
if (value < 1 || value > 5) {
console.warn('传递的值有问题!!!');
return false;
}
return true;
},
});
function setStar(newStar) {
// 如果传入 100,校验会失败,控制台会输出警告
emits('update-rating', newStar);
}
这种校验在多人协作或开发组件库时尤其重要——它能在开发阶段就发现调用方传参的问题。
小结
本文系统讲解了 Vue3 组件化的两个最核心通信机制:
- 组件基础:单文件组件
.vue包含 template、script、style 三件套。<script setup>语法糖(Vue 3.2+)让顶层变量自动暴露,配合scoped样式实现组件封装。推荐局部注册(更好的 tree-shaking 和依赖可见性),使用大驼峰命名组件。 - Props(父传子):通过
defineProps宏声明接收的数据。支持静态和动态绑定,短横线和驼峰命名互通。单向数据流——子组件不可修改 props,可通过新建 ref 或计算属性做二次处理。Props 支持完整的类型校验和自定义 validator。 - 自定义事件(子传父):通过
defineEmits宏声明事件,调用emits()触发并传参。父组件用@事件名绑定处理函数接收数据。事件也支持校验函数,确保子传父数据的正确性。
Props 和自定义事件是 Vue 组件的「任督二脉」——一个负责数据向下流,一个负责通知向上走。掌握了这两者,你就掌握了父子组件通信的完整体系。