Vue 里面有有好多组件,组件是 Vue 应用的基本构建块。通过拆分为独立、可复用的模块,从而提升代码的可维护性和复用性。
接下来,将从组件的创建与注册入手,讲解一下父子组件通信的 3 种核心方式(Props 传值、自定义事件、 refs 调用),从而增长下知识,也来消磨下时间。
1. 什么是组件?
组件(Component)是 Vue 中可复用的 Vue 实例,它包含模板(template)、逻辑(script)和样式(style),可以理解为 “自定义的 HTML 元素”。
例如,一个页面可以拆分为 Header、Content、Footer 三个组件,每个组件负责自己的功能,通过组合形成完整页面。
2. 组件的创建方式(.vue 单文件组件)
在 Vue 中,最推荐的组件创建方式是 单文件组件 ,即一个 .vue 文件对应一个组件,包含三个核心部分:
<!-- 组件文件名:MyComponent.vue -->
<template>
<!-- 模板:组件的 HTML 结构 -->
<div class="my-component">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</template>
<script setup>
// 逻辑:组件的数据、方法等
import { ref } from 'vue';
// 组件内部状态
const title = ref('我是一个组件');
const content = ref('这是组件的内容');
</script>
<style scoped>
/* 样式:组件的样式(scoped 表示样式仅作用于当前组件) */
.my-component {
padding: 16px;
border: 1px solid #eee;
border-radius: 4px;
}
</style>
<template>:必须有且只有一个根元素(Vue 3 支持多根元素),定义组件的结构;<script setup>:Vue 3 推荐的组合式 API 语法,定义组件的响应式数据、方法等;<style scoped>:scoped属性确保样式仅作用于当前组件,避免样式冲突(可选)。
二、组件的注册方式
创建组件后,需要 “注册” 才能在其他组件中使用。Vue 提供两种注册方式:局部注册和全局注册。
1. 局部注册(推荐)
局部注册的组件仅在当前组件内可用,避免全局污染,适合大多数场景。
步骤:
- 导入组件;
- 在
components选项中注册(Vue 3<script setup>中导入即注册,无需额外配置)。
<!-- 父组件:Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<!-- 使用局部注册的组件 -->
<my-component></my-component>
</div>
</template>
<script setup>
// Vue 3 <script setup> 中,导入组件后可直接使用(自动局部注册)
import MyComponent from './MyComponent.vue';
</script>
注意:组件名推荐使用 PascalCase(帕斯卡命名法,如 MyComponent) 或 kebab-case(短横线命名法,如 my-component) ,在模板中使用时两种格式均可(推荐 kebab-case 与 HTML 标签风格保持一致)。
2. 全局注册
全局注册的组件在整个应用中任何组件内都可直接使用,适合通用组件(如按钮、输入框)。
步骤:在应用初始化时,通过 app.component() 注册。
// main.js(Vue 3)
import { createApp } from 'vue';
import App from './App.vue';
// 导入全局组件
import MyButton from './components/MyButton.vue';
const app = createApp(App);
// 全局注册组件(参数:组件名,组件对象)
app.component('my-button', MyButton);
app.mount('#app');
注册后,在任何组件中无需导入即可直接使用:
<template>
<!-- 直接使用全局注册的组件 -->
<my-button></my-button>
</template>
全局注册的缺点:
- 即使组件未被使用,也会被打包到最终代码中,增加包体积;
- 可能导致命名冲突(多个组件重名时覆盖)。
建议:优先使用局部注册,仅对高频复用的基础组件(如 Button、Input)使用全局注册。
三、父子组件通信的 3 种常用方式
组件间通信是组件化开发的核心问题,其中父子组件通信是最基础、最常见的场景。以下是 3 种核心通信方式:
1. 父传子:Props 传递数据
场景:父组件向子组件传递数据(如配置项、初始值)。
原理:父组件通过 “自定义属性” 传递数据,子组件通过 props 选项接收数据。
步骤 1:子组件声明 Props
子组件通过 defineProps 宏(Vue 3 <script setup>)声明接收的属性,并指定类型、默认值等。
<!-- 子组件:Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<p>父组件传递的标题:{{ title }}</p>
<p>父组件传递的数量:{{ count }}</p>
<p>父组件传递的用户信息:{{ user.name }},{{ user.age }}岁</p>
</div>
</template>
<script setup>
// 声明接收的 props(指定类型、默认值、是否必传)
const props = defineProps({
// 字符串类型
title: {
type: String,
required: true // 必传属性
},
// 数字类型,有默认值
count: {
type: Number,
default: 0 // 默认值
},
// 对象类型(默认值需用函数返回,避免引用类型共享)
user: {
type: Object,
default: () => ({ name: '未知', age: 0 })
}
});
// 访问 props 数据(无需 .value,因为 props 是响应式的)
console.log(props.title); // 父组件传递的 title 值
</script>
步骤 2:父组件传递 Props
父组件在使用子组件时,通过 “自定义属性” 传递数据,属性名与子组件 props 声明的名称一致。
<!-- 父组件:Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<!-- 传递 Props 给子组件 -->
<child
title="这是父组件传递的标题"
:count="num" <!-- 传递响应式数据需用 v-bind 简写 : -->
:user="userInfo"
></child>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import Child from './Child.vue'; // 局部注册子组件
// 父组件的数据
const num = ref(100); // 响应式数字
const userInfo = reactive({ name: '张三', age: 20 }); // 响应式对象
</script>
注意事项
- 单向数据流:Props 是 “只读” 的,子组件不能直接修改
props的值(会报错)。若需修改,应通过 “子传父” 通知父组件修改源数据(见方式 2)。 - 类型校验:通过
type指定 props 类型(如String、Number、Object),Vue 会在开发环境校验传递的数据类型,不匹配时报警告。 - 响应式传递:父组件传递响应式数据(
ref/reactive)时,子组件会自动感知数据变化(无需额外处理)。
2. 子传父:自定义事件($emit)
场景:子组件向父组件传递数据或通知父组件执行操作(如子组件按钮点击后通知父组件更新数据)。
原理:子组件通过 $emit 触发 “自定义事件” 并传递数据,父组件通过 v-on 监听事件并接收数据。
步骤 1:子组件触发自定义事件
子组件通过 defineEmits 宏(Vue 3 <script setup>)声明可触发的事件,然后通过 emit 方法触发事件并传递数据。
<!-- 子组件:Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<button @click="handleClick">点击向父组件传值</button>
<input
type="text"
v-model="inputValue"
@input="handleInput" <!-- 输入时触发事件 -->
>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 声明可触发的自定义事件(参数为事件名数组)
const emit = defineEmits(['send-message', 'input-change']);
// 子组件内部数据
const inputValue = ref('');
// 按钮点击事件:触发 send-message 事件,传递数据
const handleClick = () => {
// 触发事件并传递参数(可传多个)
emit('send-message', 'Hello 父组件', 123);
};
// 输入事件:触发 input-change 事件,传递输入值
const handleInput = () => {
emit('input-change', inputValue.value);
};
</script>
步骤 2:父组件监听自定义事件
父组件在使用子组件时,通过 v-on(简写 @)监听子组件触发的事件,并在事件处理函数中接收数据。
<!-- 父组件:Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<!-- 监听子组件的自定义事件 -->
<child
@send-message="handleMessage"
@input-change="handleInputChange"
></child>
<p>子组件传递的消息:{{ childMessage }}</p>
<p>子组件输入的值:{{ childInputValue }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
// 父组件存储子组件传递的数据
const childMessage = ref('');
const childInputValue = ref('');
// 处理 send-message 事件(接收子组件传递的参数)
const handleMessage = (msg, num) => {
console.log('子组件传递的参数:', msg, num); // 输出 "Hello 父组件" 123
childMessage.value = msg;
};
// 处理 input-change 事件
const handleInputChange = (value) => {
childInputValue.value = value;
};
</script>
进阶:事件参数校验(Vue 3.3+)
Vue 3.3+ 支持对自定义事件的参数进行类型校验,增强代码健壮性:
<script setup>
// 用对象形式声明事件,指定参数类型
const emit = defineEmits({
// 校验 send-message 事件的参数(msg 为 string,num 为 number)
'send-message': (msg, num) => {
if (typeof msg !== 'string' || typeof num !== 'number') {
console.warn('send-message 事件参数类型错误');
return false; // 校验失败
}
return true; // 校验成功
}
});
</script>
3. 父调用子:refs 获取子组件实例 / 元素
场景:父组件需要直接访问子组件的属性或方法(如调用子组件的初始化方法、获取子组件的 DOM 元素)。
原理:父组件通过 ref 属性给子组件指定标识,然后通过 $refs 访问子组件实例或 DOM 元素。
步骤 1:子组件暴露属性和方法
子组件通过 defineExpose 宏(Vue 3 <script setup>)主动暴露需要被父组件访问的属性和方法(<script setup> 中声明的内容默认是私有的)。
<!-- 子组件:Child.vue -->
<template>
<div class="child" ref="childDom">
<h3>子组件</h3>
<p>子组件计数器:{{ count }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 子组件内部状态
const count = ref(0);
// 子组件DOM元素(通过 ref 获取)
const childDom = ref(null);
// 子组件方法:增加计数
const increment = () => {
count.value++;
};
// 子组件方法:重置计数
const reset = () => {
count.value = 0;
};
// 组件挂载后打印DOM元素
onMounted(() => {
console.log('子组件DOM:', childDom.value);
});
// 暴露属性和方法给父组件(父组件通过 refs 只能访问这里暴露的内容)
defineExpose({
count, // 暴露响应式数据
increment, // 暴露方法
reset,
childDom // 暴露DOM元素引用
});
</script>
步骤 2:父组件通过 refs 访问子组件
父组件在子组件上添加 ref 属性指定名称,然后通过 ref 变量访问子组件暴露的内容(需在组件挂载后访问,否则为 null)。
<!-- 父组件:Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<!-- 给子组件添加 ref 属性 -->
<child ref="childRef"></child>
<button @click="callChildMethod">调用子组件方法</button>
<button @click="getChildData">获取子组件数据</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
// 创建 ref 变量关联子组件(名称与模板中 ref 属性一致)
const childRef = ref(null);
// 组件挂载后才能访问到子组件实例
onMounted(() => {
console.log('子组件实例:', childRef.value); // 输出子组件暴露的内容
});
// 调用子组件的方法
const callChildMethod = () => {
if (childRef.value) {
childRef.value.increment(); // 调用子组件的 increment 方法
}
};
// 获取子组件的数据
const getChildData = () => {
if (childRef.value) {
console.log('子组件当前计数:', childRef.value.count.value); // 注意 ref 数据需 .value
console.log('子组件DOM:', childRef.value.childDom.value); // 访问子组件的DOM
}
};
</script>
注意事项
- 访问时机:子组件的
ref变量在组件挂载前为null,需在onMounted钩子或点击事件等挂载后触发的逻辑中访问。 - 避免过度使用:
refs打破了组件的封装性,父组件直接依赖子组件的内部实现,不利于维护。优先使用 Props 和自定义事件,仅在必要时(如调用子组件方法)使用refs。 - 响应式访问:若访问子组件暴露的
ref数据(如childRef.value.count),需通过.value访问其值(因为count是ref类型)。
四、总结
组件是 Vue 开发的核心,掌握以下要点能大幅提升组件化开发能力:
-
组件创建:使用
.vue单文件组件,分离模板、逻辑和样式; -
组件注册:优先局部注册,通用组件全局注册;
-
父子通信:
- 父传子:通过 Props 传递数据,子组件声明接收并使用;
- 子传父:子组件
emit自定义事件传递数据,父组件监听事件; - 父调子:通过 refs 访问子组件暴露的属性和方法(谨慎使用)。
总之,合理进行拆分组件,处理好界面之间的交互,能让项目结构清晰,易于开发。就像搞好男女朋友那样,后续还不是水到渠成吗?