组件通信与注册
文章目录
- 前言
- 一、通信方式总览
- 1.1 选型指南
- 二、Props / Emit(父子通信)
- 2.1 单向数据流
- 2.2 v-model 本质
- 2.3 常见场景
- 三、provide / inject(跨层级通信)
- 3.1 基本用法
- 3.2 响应式 provide
- 3.3 应用场景
- 3.4 易混淆点
- 四、事件总线 mitt
- 4.1 Vue 3 的变化
- 4.2 封装与使用
- 4.3 适用场景
- 五、Pinia / Vuex(全局状态)
- 5.1 何时使用
- 5.2 Pinia 基本用法
- 5.3 与其他方式对比
- 六、组件注册方式
- 6.1 全局注册
- 6.2 局部注册
- 6.3 异步组件注册
- 6.4 全局 vs 局部
- 七、通信方式对比总结
- 八、面试聚焦
- 8.1 Props 单向数据流
- 8.2 provide/inject 响应式
- 8.3 全局注册无法 Tree-shaking
- 8.4 Vue 3 事件总线
- 九、易混淆点
- 十、思考与练习
- 总结
前言
组件化开发的核心问题之一,就是组件之间如何传递数据和触发行为。Vue 提供了多种通信方式,本篇会讲清楚:
- Props / Emit(父子通信)
- provide / inject(跨层级通信)
- 事件总线 mitt
- Pinia / Vuex(全局状态)
- 组件注册方式(全局 / 局部 / 异步)
一、通信方式总览
1.1 选型指南
| 方式 | 适用场景 | 关系 |
|---|---|---|
| Props / Emit | 父子数据传递、子通知父 | 直接父子 |
| provide / inject | 主题、语言包、表单上下文 | 祖孙跨层级 |
| mitt(事件总线) | 兄弟组件、无关联组件 | 任意组件 |
| Pinia / Vuex | 用户状态、权限、购物车 | 全局共享 |
// 选型原则:// 1. 能用 Props/Emit 解决的,优先用 Props/Emit(数据流清晰)// 2. 跨多层级透传 → provide/inject// 3. 无关联组件 → mitt 或 Pinia// 4. 多处共享的全局状态 → Pinia二、Props / Emit(父子通信)
2.1 单向数据流
<!-- 父组件 Parent.vue --> <script setup> import { ref } from 'vue' import Child from './Child.vue' const count = ref(0) const handleChange = (val) => { count.value = val // 父组件修改数据 } </script> <template> <Child :count="count" @change="handleChange" /> </template><!-- 子组件 Child.vue --> <script setup> const props = defineProps({ count: { type: Number, default: 0 } }) const emit = defineEmits(['change']) const increment = () => { // ❌ 不能直接修改 props // props.count++ // ✅ 通过 emit 通知父组件 emit('change', props.count + 1) } </script> <template> <button @click="increment">{{ count }}</button> </template>2.2 v-model 本质
<!-- v-model 是 Props + Emit 的语法糖 --> <MyInput v-model="text" /> <!-- 等价于 --> <MyInput :modelValue="text" @update:modelValue="text = $event" /> <!-- 多个 v-model --> <MyForm v-model:name="name" v-model:age="age" />2.3 常见场景
// 1. 父传配置:列表组件接收 items 和 loading<List:items="list":loading="loading"/>// 2. 子通知父:表单提交后 emit submit 事件// emit('submit', formData)// 3. 分页:子组件 emit page-change,父组件加载数据// emit('page-change', page)三、provide / inject(跨层级通信)
3.1 基本用法
// 祖先组件import{provide,ref}from'vue'consttheme=ref('dark')provide('theme',theme)// 后代组件(任意层级)import{inject}from'vue'consttheme=inject('theme','light')// 第二个参数是默认值3.2 响应式 provide
// ❌ 默认不是响应式:传递普通值provide('count',0)// 后代无法感知变化// ✅ 传递 ref 或 reactive 实现响应式constcount=ref(0)provide('count',count)// 后代组件constcount=inject('count')// count 变化时,后代视图自动更新3.3 应用场景
// 1. 主题配置provide('theme',{color:'primary',size:'medium'})// 2. 国际化provide('locale',locale)// 3. 表单上下文(Form → FormItem)provide('formContext',{rules,validate})// 4. 全局 HTTP 实例app.provide('http',axios.create({baseURL:'/api'}))3.4 易混淆点
// 1. 多个祖先 provide 同名 key → 取最近祖先的值// 2. inject 可指定默认值,找不到 provider 不会报错// 3. app.provide 应用级注入,任何组件都可 inject// 4. 过度使用会导致数据流难追踪,简单场景优先 Props四、事件总线 mitt
4.1 Vue 3 的变化
// Vue 2:实例方法constbus=newVue()bus.$on('message',handler)bus.$emit('message',data)bus.$off('message',handler)// Vue 3:$on/$off/$once 已移除,使用 mittimportmittfrom'mitt'constbus=mitt()bus.on('message',(data)=>console.log(data))bus.emit('message',{text:'Hello'})bus.off('message',handler)4.2 封装与使用
// utils/eventBus.jsimportmittfrom'mitt'exportconsteventBus=mitt()// 组件 A:发送import{eventBus}from'@/utils/eventBus'eventBus.emit('refresh-list')// 组件 B:接收import{onMounted,onUnmounted}from'vue'import{eventBus}from'@/utils/eventBus'consthandler=()=>fetchList()onMounted(()=>eventBus.on('refresh-list',handler))onUnmounted(()=>eventBus.off('refresh-list',handler))4.3 适用场景
// ✅ 适合:兄弟组件、无直接关系的组件间通信// 如:Header 通知 Sidebar 刷新// ❌ 不适合:复杂全局状态(用 Pinia)// ❌ 不适合:父子通信(用 Props/Emit,更清晰)五、Pinia / Vuex(全局状态)
5.1 何时使用
// 适合 Pinia 的场景:// 1. 用户登录态、Token、用户信息// 2. 购物车、收藏夹// 3. 应用全局配置(主题、语言、侧边栏状态)// 4. 多处页面共享的缓存数据5.2 Pinia 基本用法
// stores/user.jsimport{defineStore}from'pinia'exportconstuseUserStore=defineStore('user',{state:()=>({name:'',token:''}),getters:{isLoggedIn:(state)=>!!state.token},actions:{login(token){this.token=token},logout(){this.token=''this.name=''}}})// 组件中使用import{useUserStore}from'@/stores/user'import{storeToRefs}from'pinia'constuserStore=useUserStore()const{name,isLoggedIn}=storeToRefs(userStore)// 保持响应性userStore.login('abc123')5.3 与其他方式对比
| 方式 | 数据范围 | 持久化 | 适用 |
|---|---|---|---|
| Props/Emit | 父子 | 否 | 局部数据 |
| provide/inject | 组件树 | 否 | 主题、上下文 |
| mitt | 任意 | 否 | 一次性通知 |
| Pinia | 全局 | 可插件持久化 | 共享状态 |
六、组件注册方式
6.1 全局注册
import{createApp}from'vue'importAppfrom'./App.vue'importMyButtonfrom'./components/MyButton.vue'constapp=createApp(App)// 全局注册:任何模板中可直接使用app.component('MyButton',MyButton)app.mount('#app')<!-- 任意组件模板中 --> <template> <MyButton>点击</MyButton> </template>6.2 局部注册
<!-- 推荐:<script setup> 中导入即局部注册 --> <script setup> import MyButton from './MyButton.vue' import UserCard from './UserCard.vue' // 无需额外声明,导入即可在模板中使用 </script> <template> <MyButton /> <UserCard /> </template>6.3 异步组件注册
import{defineAsyncComponent}from'vue'// 局部异步组件constHeavyModal=defineAsyncComponent(()=>import('./HeavyModal.vue'))// 全局异步注册app.component('HeavyModal',defineAsyncComponent(()=>import('./HeavyModal.vue')))// 带加载和错误状态constAsyncComp=defineAsyncComponent({loader:()=>import('./MyComponent.vue'),loadingComponent:LoadingSpinner,errorComponent:ErrorDisplay,delay:200,timeout:30000})6.4 全局 vs 局部
| 对比项 | 全局注册 | 局部注册 |
|---|---|---|
| 使用范围 | 任意组件 | 当前组件 |
| Tree-shaking | 不支持,未使用也会打包 | 支持 |
| 依赖关系 | 不明确 | 清晰 |
| 适用 | 基础通用组件(Button、Icon) | 业务页面组件 |
// 全局注册必须在 app.mount() 之前完成// <script setup> 导入的 .vue 文件自动局部注册// 组件名推荐 PascalCase,模板中可用 kebab-case七、通信方式对比总结
父子直接通信 → Props / Emit 跨多层级透传 → provide / inject 兄弟/无关联组件 → mitt 或 Pinia 全局共享状态 → Pinia 基础 UI 组件 → 全局注册 业务页面组件 → 局部注册 + 异步加载八、面试聚焦
8.1 Props 单向数据流
// 子组件不能直接修改 props// 应通过 emit 通知父组件修改emit('update:count',newValue)8.2 provide/inject 响应式
// 默认不是响应式// 需要传递 ref 或 reactiveprovide('theme',ref('dark'))8.3 全局注册无法 Tree-shaking
// 全局注册的组件即使未使用也会被打包// 业务组件应局部注册,支持 Tree-shaking8.4 Vue 3 事件总线
// Vue 3 移除 $on/$off/$emit// 使用 mitt 库实现事件总线九、易混淆点
- Props 是单向数据流:子组件不能直接修改 prop,应通过 emit 通知父组件。
- provide/inject 默认非响应式:传递
ref或reactive才能实现响应式更新。 - mitt vs Pinia:mitt 适合一次性通知,Pinia 适合需要持久化的全局状态。
- 全局注册无法 Tree-shaking:未使用的全局组件仍会打包,业务组件应局部注册。
- defineProps / defineEmits:编译器宏,无需导入,不能在条件语句中使用。
十、思考与练习
1.Vue 组件通信有哪些方式?各自适用场景?
解析:
- Props/Emit:父子直接通信
- provide/inject:跨层级(主题、表单上下文)
- mitt:兄弟或无关联组件
- Pinia:全局共享状态
2.为什么子组件不能直接修改 props?
解析:Vue 遵循单向数据流,props 由父组件控制。子组件修改 props 会破坏数据流的可预测性,应通过 emit 通知父组件修改。
3.provide/inject 如何实现响应式?
解析:传递ref或reactive对象,而不是普通值:
provide('count',ref(0))// ✅ 响应式provide('count',0)// ❌ 非响应式4.Vue 3 如何实现事件总线?
解析:使用 mitt 库替代 Vue 2 的$on/$off/$emit:
importmittfrom'mitt'constbus=mitt()bus.on('event',handler)bus.emit('event',data)5.全局注册和局部注册如何选择?
解析:
- 全局注册:基础通用组件(Button、Input),减少重复导入
- 局部注册:业务组件,依赖清晰,支持 Tree-shaking
总结
- Props/Emit:父子通信,单向数据流,v-model 是其语法糖
- provide/inject:跨层级通信,传递 ref/reactive 实现响应式
- mitt:Vue 3 事件总线,替代o n / on/on/off
- Pinia:全局状态管理,适合登录态、购物车等
- 组件注册:全局(通用 UI)vs 局部(业务组件)vs 异步(按需加载)