在初学 Vue 时,对组件概念和通信逻辑一直似懂非懂。经过实际项目开发沉淀后,重新梳理组件全套核心知识点,包含组件意义、单文件组件、组件关系、全套通信方式,条理清晰,如果错误,欢迎指正。
一、为什么要使用组件?(组件化核心价值)
-
代码复用,减少重复开发:通用模块(按钮、弹窗、分页、卡片)只需编写一次,全局多处复用,大幅减少冗余代码。
-
代码拆分,结构清晰:将庞大页面按功能拆分为多个小组件,分而治之,代码层级清晰,便于阅读查找。
-
独立作用域,防止污染:组件 JS 变量私有、CSS 可通过 scoped 隔离,避免全局变量、样式冲突。
-
逻辑封装,降低维护成本:复杂业务逻辑封装在组件内部,外部只需调用即可,需求变更只需修改对应组件。
-
按需加载,优化页面性能:配合路由懒加载、异步组件,非首屏组件按需加载,减小首屏资源体积,提升页面渲染速度。
二、什么是单文件组件(SFC)
单文件组件是 Vue 工程化项目的标准开发方式,后缀为 .vue,将一个组件的结构、逻辑、样式三合一封装在同一个文件中。
一个标准单文件组件包含三部分:
-
<template>:书写页面 HTML 结构 -
<script setup>:书写组件 JS 业务逻辑、数据、方法、导入依赖 -
<style scoped>:书写当前组件专属样式(scoped 实现样式隔离)
三、组件之间的关系
项目中所有组件通信,都基于三种层级关系:
-
父子组件:组件直接嵌套引用(最常用)
-
兄弟组件:共用同一个父组件,平级互不嵌套
-
祖孙组件:多层嵌套(爷爷-父-孙子),隔层级关系
组件核心难点:不同关系的组件数据传递(通信)
四、父子组件通信(最核心)
1. 父传子:props 单向传值
原理
父组件通过自定义属性绑定数据,子组件通过 props 接收数据,单向数据流:只能父改子,子不能直接修改 props。
代码示例
父组件 Parent.vue
vue
<template>
<!-- 动态绑定数据传给子组件 -->
<Child :msg="title" :list="tableList" />
</template>
<script setup>
import Child from './Child.vue'
const title = '父组件传递的文字'
const tableList = [1,2,3]
</script>
子组件 Child.vue
vue
<template>
<div>
<p>{{ msg }}</p>
<p>{{ list }}</p>
</div>
</template>
<script setup>
// 简单接收
// const props = defineProps(['msg', 'list'])
// 项目规范写法:类型校验 + 必填 + 默认值
const props = defineProps({
msg: {
type: String,
required: true
},
list: {
type: Array,
default: () => [] // 引用类型默认值必须通过函数返回
}
})
console.log(props.msg)
</script>
2. 子传父:defineEmits 自定义事件
原理
子组件通过 defineEmits 声明自定义事件,调用 emit 触发事件并携带参数;父组件监听对应事件,接收子组件数据。
代码示例
子组件 Child.vue
vue
<template>
<button @click="sendData">向父组件传值</button>
</template>
<script setup>
// 声明自定义事件
const emit = defineEmits(['send'])
const sendData = () => {
// 触发事件,携带多个参数传给父组件
emit('send', '子组件的数据', 1232321)
}
</script>
父组件 Parent.vue
vue
<template>
<Child @send="getChildData" />
</template>
<script setup>
// 接收子组件传递的参数
const getChildData = (val1, val2) => {
console.log(val1, val2)
}
</script>
3. 父子双向绑定语法糖:defineModel
本质:props + emit 的语法糖,简化双向绑定代码,Vue3 推荐使用。
代码示例
子组件
vue
<template>
<input v-model="modelValue" />
</template>
<script setup>
// 快速实现双向绑定
const modelValue = defineModel()
</script>
父组件
vue
<template>
<Child v-model="num" />
</template>
<script setup>
const num = ref(0)
</script>
4. 父获取子实例/方法:ref + defineExpose
原理
默认子组件内部属性、方法对外封闭,子组件通过 defineExpose 主动暴露内容,父组件通过 ref 获取子组件实例,调用子组件变量和方法。
代码示例
子组件 Child.vue
vue
<script setup>
import { ref } from 'vue'
const childMsg = ref('子内部变量')
const childFn = () => {
alert('子组件方法')
}
// 主动暴露给父组件
defineExpose({ childMsg, childFn })
</script>
父组件 Parent.vue
vue
<template>
<Child ref="childRef" />
<button @click="callChild">调用子方法</button>
</template>
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
const callChild = () => {
console.log(childRef.value.childMsg)
childRef.value.childFn()
}
</script>
五、祖孙组件通信(跨层级)
祖孙组件多层嵌套,不推荐 props 逐层透传(中间组件冗余),主流两种方案:provide/inject、事件总线 mitt。
1. provide / inject(Vue 原生首选)
核心原理
-
provide:祖先组件(爷爷)提供数据/方法,向下穿透所有后代组件 -
inject:任意后代组件直接注入使用,跳过中间所有层级
1.1 传递静态数据
爷爷组件
vue
<template>
<Father />
</template>
<script setup>
import { provide } from 'vue'
provide('grandMsg', '爷爷的静态文字')
</script>
孙子组件
vue
<script setup>
import { inject } from 'vue'
const msg = inject('grandMsg')
console.log(msg)
</script>
1.2 传递响应式数据(项目常用)
普通静态数据无响应式,祖先更新数据后代不生效,需传递 ref 响应式数据。
爷爷组件
vue
<script setup>
import { ref, provide } from 'vue'
const grandNum = ref(100)
// 提供响应式数据
provide('grandNum', grandNum)
// 提供修改数据的方法
const changeNum = () => {
grandNum.value += 10
}
provide('changeNum', changeNum)
</script>
孙子组件
vue
<template>
<p>爷爷数字:{{ num }}</p>
<button @click="addFn">调用爷爷方法修改</button>
</template>
<script setup>
import { inject } from 'vue'
const num = inject('grandNum')
const addFn = inject('changeNum')
</script>
1.3 孙子向祖先反向传值
思路:爷爷向外暴露修改自身数据的方法,孙子调用该方法完成传值。
爷爷组件
vue
<script setup>
import { ref, provide } from 'vue'
const text = ref('')
const setText = (val) => {
text.value = val
}
provide('setText', setText)
</script>
孙子组件
vue
<script setup>
import { inject } from 'vue'
const setText = inject('setText')
const sendToGrand = () => {
setText('孙子传给爷爷的数据')
}
</script>
1.4 inject 默认值(防报错)
js
// 若祖先未提供对应数据,使用默认空值,避免报错
const msg = inject('grandMsg', '')
2. 祖孙通信:mitt 事件总线
原理
通过全局总线实现 孙子 → 爷爷 主动传值,适合后代主动触发通知的场景。
孙子组件(发送数据)
js
import { bus } from '@/utils/bus'
bus.emit('son-to-grand', '孙子消息')
爷爷组件(监听数据)
js
import { bus } from '@/utils/bus'
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
bus.on('son-to-grand', (data) => {
console.log('收到孙子数据', data)
})
})
// 组件销毁必须解绑,防止内存泄漏
onUnmounted(() => {
bus.off('son-to-grand')
})
优缺点
✅ 适合子孙主动向顶层祖先发送通知、回调
❌ 需手动解绑事件,大型项目易出现事件名冲突
六、核心总结
-
组件化核心:复用、拆分、隔离、易维护、高性能
-
单文件组件:.vue 文件,结构、逻辑、样式三位一体
-
父子通信:props(父传子)、emit(子传父)、defineModel(双向绑定)、ref(父控子)
-
祖孙通信:优先 provide/inject (原生简洁),主动通知用mitt
(注:部分内容可能由 AI 生成)