组件通信与注册

📅 2026/6/16 18:47:59
组件通信与注册
文章目录前言一、通信方式总览1.1 选型指南二、Props / Emit父子通信2.1 单向数据流2.2 v-model 本质2.3 常见场景三、provide / inject跨层级通信3.1 基本用法3.2 响应式 provide3.3 应用场景3.4 易混淆点四、事件总线 mitt4.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-shaking8.4 Vue 3 事件总线九、易混淆点十、思考与练习总结前言组件化开发的核心问题之一就是组件之间如何传递数据和触发行为。Vue 提供了多种通信方式本篇会讲清楚Props / Emit父子通信provide / inject跨层级通信事件总线 mittPinia / 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 :countcount changehandleChange / /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 clickincrement{{ count }}/button /template2.2 v-model 本质!-- v-model 是 Props Emit 的语法糖 -- MyInput v-modeltext / !-- 等价于 -- MyInput :modelValuetext update:modelValuetext $event / !-- 多个 v-model -- MyForm v-model:namename v-model:ageage /2.3 常见场景// 1. 父传配置列表组件接收 items 和 loadingList:itemslist:loadingloading/// 2. 子通知父表单提交后 emit submit 事件// emit(submit, formData)// 3. 分页子组件 emit page-change父组件加载数据// emit(page-change, page)三、provide / inject跨层级通信3.1 基本用法// 祖先组件import{provide,ref}fromvueconstthemeref(dark)provide(theme,theme)// 后代组件任意层级import{inject}fromvueconstthemeinject(theme,light)// 第二个参数是默认值3.2 响应式 provide// ❌ 默认不是响应式传递普通值provide(count,0)// 后代无法感知变化// ✅ 传递 ref 或 reactive 实现响应式constcountref(0)provide(count,count)// 后代组件constcountinject(count)// count 变化时后代视图自动更新3.3 应用场景// 1. 主题配置provide(theme,{color:primary,size:medium})// 2. 国际化provide(locale,locale)// 3. 表单上下文Form → FormItemprovide(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四、事件总线 mitt4.1 Vue 3 的变化// Vue 2实例方法constbusnewVue()bus.$on(message,handler)bus.$emit(message,data)bus.$off(message,handler)// Vue 3$on/$off/$once 已移除使用 mittimportmittfrommittconstbusmitt()bus.on(message,(data)console.log(data))bus.emit(message,{text:Hello})bus.off(message,handler)4.2 封装与使用// utils/eventBus.jsimportmittfrommittexportconsteventBusmitt()// 组件 A发送import{eventBus}from/utils/eventBuseventBus.emit(refresh-list)// 组件 B接收import{onMounted,onUnmounted}fromvueimport{eventBus}from/utils/eventBusconsthandler()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}frompiniaexportconstuseUserStoredefineStore(user,{state:()({name:,token:}),getters:{isLoggedIn:(state)!!state.token},actions:{login(token){this.tokentoken},logout(){this.tokenthis.name}}})// 组件中使用import{useUserStore}from/stores/userimport{storeToRefs}frompiniaconstuserStoreuseUserStore()const{name,isLoggedIn}storeToRefs(userStore)// 保持响应性userStore.login(abc123)5.3 与其他方式对比方式数据范围持久化适用Props/Emit父子否局部数据provide/inject组件树否主题、上下文mitt任意否一次性通知Pinia全局可插件持久化共享状态六、组件注册方式6.1 全局注册import{createApp}fromvueimportAppfrom./App.vueimportMyButtonfrom./components/MyButton.vueconstappcreateApp(App)// 全局注册任何模板中可直接使用app.component(MyButton,MyButton)app.mount(#app)!-- 任意组件模板中 -- template MyButton点击/MyButton /template6.2 局部注册!-- 推荐script setup 中导入即局部注册 -- script setup import MyButton from ./MyButton.vue import UserCard from ./UserCard.vue // 无需额外声明导入即可在模板中使用 /script template MyButton / UserCard / /template6.3 异步组件注册import{defineAsyncComponent}fromvue// 局部异步组件constHeavyModaldefineAsyncComponent(()import(./HeavyModal.vue))// 全局异步注册app.component(HeavyModal,defineAsyncComponent(()import(./HeavyModal.vue)))// 带加载和错误状态constAsyncCompdefineAsyncComponent({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 Piniamitt 适合一次性通知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/$emitimportmittfrommittconstbusmitt()bus.on(event,handler)bus.emit(event,data)5.全局注册和局部注册如何选择解析全局注册基础通用组件Button、Input减少重复导入局部注册业务组件依赖清晰支持 Tree-shaking总结Props/Emit父子通信单向数据流v-model 是其语法糖provide/inject跨层级通信传递 ref/reactive 实现响应式mittVue 3 事件总线替代o n / on/on/offPinia全局状态管理适合登录态、购物车等组件注册全局通用 UIvs 局部业务组件vs 异步按需加载