组件通信与注册

文章目录

  • 前言
  • 一、通信方式总览
    • [1.1 选型指南](#1.1 选型指南)
  • [二、Props / Emit(父子通信)](#二、Props / Emit(父子通信))
    • [2.1 单向数据流](#2.1 单向数据流)
    • [2.2 v-model 本质](#2.2 v-model 本质)
    • [2.3 常见场景](#2.3 常见场景)
  • [三、provide / inject(跨层级通信)](#三、provide / inject(跨层级通信))
    • [3.1 基本用法](#3.1 基本用法)
    • [3.2 响应式 provide](#3.2 响应式 provide)
    • [3.3 应用场景](#3.3 应用场景)
    • [3.4 易混淆点](#3.4 易混淆点)
  • [四、事件总线 mitt](#四、事件总线 mitt)
    • [4.1 Vue 3 的变化](#4.1 Vue 3 的变化)
    • [4.2 封装与使用](#4.2 封装与使用)
    • [4.3 适用场景](#4.3 适用场景)
  • [五、Pinia / Vuex(全局状态)](#五、Pinia / Vuex(全局状态))
    • [5.1 何时使用](#5.1 何时使用)
    • [5.2 Pinia 基本用法](#5.2 Pinia 基本用法)
    • [5.3 与其他方式对比](#5.3 与其他方式对比)
  • 六、组件注册方式
    • [6.1 全局注册](#6.1 全局注册)
    • [6.2 局部注册](#6.2 局部注册)
    • [6.3 异步组件注册](#6.3 异步组件注册)
    • [6.4 全局 vs 局部](#6.4 全局 vs 局部)
  • 七、通信方式对比总结
  • 八、面试聚焦
    • [8.1 Props 单向数据流](#8.1 Props 单向数据流)
    • [8.2 provide/inject 响应式](#8.2 provide/inject 响应式)
    • [8.3 全局注册无法 Tree-shaking](#8.3 全局注册无法 Tree-shaking)
    • [8.4 Vue 3 事件总线](#8.4 Vue 3 事件总线)
  • 九、易混淆点
  • 十、思考与练习
  • 总结

前言

组件化开发的核心问题之一,就是组件之间如何传递数据和触发行为。Vue 提供了多种通信方式,本篇会讲清楚:

  • Props / Emit(父子通信)
  • provide / inject(跨层级通信)
  • 事件总线 mitt
  • Pinia / Vuex(全局状态)
  • 组件注册方式(全局 / 局部 / 异步)

一、通信方式总览

1.1 选型指南

方式 适用场景 关系
Props / Emit 父子数据传递、子通知父 直接父子
provide / inject 主题、语言包、表单上下文 祖孙跨层级
mitt(事件总线) 兄弟组件、无关联组件 任意组件
Pinia / Vuex 用户状态、权限、购物车 全局共享
javascript 复制代码
// 选型原则:
// 1. 能用 Props/Emit 解决的,优先用 Props/Emit(数据流清晰)
// 2. 跨多层级透传 → provide/inject
// 3. 无关联组件 → mitt 或 Pinia
// 4. 多处共享的全局状态 → Pinia

二、Props / Emit(父子通信)

2.1 单向数据流

vue 复制代码
<!-- 父组件 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>
vue 复制代码
<!-- 子组件 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 本质

vue 复制代码
<!-- 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 常见场景

javascript 复制代码
// 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 基本用法

javascript 复制代码
// 祖先组件
import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)

// 后代组件(任意层级)
import { inject } from 'vue'

const theme = inject('theme', 'light')  // 第二个参数是默认值

3.2 响应式 provide

javascript 复制代码
// ❌ 默认不是响应式:传递普通值
provide('count', 0)  // 后代无法感知变化

// ✅ 传递 ref 或 reactive 实现响应式
const count = ref(0)
provide('count', count)

// 后代组件
const count = inject('count')
// count 变化时,后代视图自动更新

3.3 应用场景

javascript 复制代码
// 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 易混淆点

javascript 复制代码
// 1. 多个祖先 provide 同名 key → 取最近祖先的值
// 2. inject 可指定默认值,找不到 provider 不会报错
// 3. app.provide 应用级注入,任何组件都可 inject
// 4. 过度使用会导致数据流难追踪,简单场景优先 Props

四、事件总线 mitt

4.1 Vue 3 的变化

javascript 复制代码
// Vue 2:实例方法
const bus = new Vue()
bus.$on('message', handler)
bus.$emit('message', data)
bus.$off('message', handler)

// Vue 3:$on/$off/$once 已移除,使用 mitt
import mitt from 'mitt'
const bus = mitt()

bus.on('message', (data) => console.log(data))
bus.emit('message', { text: 'Hello' })
bus.off('message', handler)

4.2 封装与使用

javascript 复制代码
// utils/eventBus.js
import mitt from 'mitt'
export const eventBus = mitt()

// 组件 A:发送
import { eventBus } from '@/utils/eventBus'
eventBus.emit('refresh-list')

// 组件 B:接收
import { onMounted, onUnmounted } from 'vue'
import { eventBus } from '@/utils/eventBus'

const handler = () => fetchList()
onMounted(() => eventBus.on('refresh-list', handler))
onUnmounted(() => eventBus.off('refresh-list', handler))

4.3 适用场景

javascript 复制代码
// ✅ 适合:兄弟组件、无直接关系的组件间通信
// 如:Header 通知 Sidebar 刷新

// ❌ 不适合:复杂全局状态(用 Pinia)
// ❌ 不适合:父子通信(用 Props/Emit,更清晰)

五、Pinia / Vuex(全局状态)

5.1 何时使用

javascript 复制代码
// 适合 Pinia 的场景:
// 1. 用户登录态、Token、用户信息
// 2. 购物车、收藏夹
// 3. 应用全局配置(主题、语言、侧边栏状态)
// 4. 多处页面共享的缓存数据

5.2 Pinia 基本用法

javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = 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'

const userStore = useUserStore()
const { name, isLoggedIn } = storeToRefs(userStore)  // 保持响应性
userStore.login('abc123')

5.3 与其他方式对比

方式 数据范围 持久化 适用
Props/Emit 父子 局部数据
provide/inject 组件树 主题、上下文
mitt 任意 一次性通知
Pinia 全局 可插件持久化 共享状态

六、组件注册方式

6.1 全局注册

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'

const app = createApp(App)

// 全局注册:任何模板中可直接使用
app.component('MyButton', MyButton)

app.mount('#app')
vue 复制代码
<!-- 任意组件模板中 -->
<template>
  <MyButton>点击</MyButton>
</template>

6.2 局部注册

vue 复制代码
<!-- 推荐:<script setup> 中导入即局部注册 -->
<script setup>
import MyButton from './MyButton.vue'
import UserCard from './UserCard.vue'
// 无需额外声明,导入即可在模板中使用
</script>

<template>
  <MyButton />
  <UserCard />
</template>

6.3 异步组件注册

javascript 复制代码
import { defineAsyncComponent } from 'vue'

// 局部异步组件
const HeavyModal = defineAsyncComponent(() =>
  import('./HeavyModal.vue')
)

// 全局异步注册
app.component('HeavyModal', defineAsyncComponent(() =>
  import('./HeavyModal.vue')
))

// 带加载和错误状态
const AsyncComp = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 30000
})

6.4 全局 vs 局部

对比项 全局注册 局部注册
使用范围 任意组件 当前组件
Tree-shaking 不支持,未使用也会打包 支持
依赖关系 不明确 清晰
适用 基础通用组件(Button、Icon) 业务页面组件
javascript 复制代码
// 全局注册必须在 app.mount() 之前完成
// <script setup> 导入的 .vue 文件自动局部注册
// 组件名推荐 PascalCase,模板中可用 kebab-case

七、通信方式对比总结

复制代码
父子直接通信     → Props / Emit
跨多层级透传     → provide / inject
兄弟/无关联组件  → mitt 或 Pinia
全局共享状态     → Pinia
基础 UI 组件     → 全局注册
业务页面组件     → 局部注册 + 异步加载

八、面试聚焦

8.1 Props 单向数据流

javascript 复制代码
// 子组件不能直接修改 props
// 应通过 emit 通知父组件修改
emit('update:count', newValue)

8.2 provide/inject 响应式

javascript 复制代码
// 默认不是响应式
// 需要传递 ref 或 reactive
provide('theme', ref('dark'))

8.3 全局注册无法 Tree-shaking

javascript 复制代码
// 全局注册的组件即使未使用也会被打包
// 业务组件应局部注册,支持 Tree-shaking

8.4 Vue 3 事件总线

javascript 复制代码
// Vue 3 移除 $on/$off/$emit
// 使用 mitt 库实现事件总线

九、易混淆点

  1. Props 是单向数据流:子组件不能直接修改 prop,应通过 emit 通知父组件。
  2. provide/inject 默认非响应式 :传递 refreactive 才能实现响应式更新。
  3. mitt vs Pinia:mitt 适合一次性通知,Pinia 适合需要持久化的全局状态。
  4. 全局注册无法 Tree-shaking:未使用的全局组件仍会打包,业务组件应局部注册。
  5. defineProps / defineEmits:编译器宏,无需导入,不能在条件语句中使用。

十、思考与练习

1. Vue 组件通信有哪些方式?各自适用场景?

解析:

  • Props/Emit:父子直接通信
  • provide/inject:跨层级(主题、表单上下文)
  • mitt:兄弟或无关联组件
  • Pinia:全局共享状态

2. 为什么子组件不能直接修改 props?

解析:Vue 遵循单向数据流,props 由父组件控制。子组件修改 props 会破坏数据流的可预测性,应通过 emit 通知父组件修改。

3. provide/inject 如何实现响应式?

解析:传递 refreactive 对象,而不是普通值:

javascript 复制代码
provide('count', ref(0))  // ✅ 响应式
provide('count', 0)       // ❌ 非响应式

4. Vue 3 如何实现事件总线?

解析:使用 mitt 库替代 Vue 2 的 $on/$off/$emit

javascript 复制代码
import mitt from 'mitt'
const bus = 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 异步(按需加载)
相关推荐
VidDown14 小时前
显卡处理视频技术详解:从硬解码到 NVENC,GPU 如何让视频处理起飞?
javascript·编辑器·音视频·视频编解码·视频
jay神14 小时前
基于 FastAPI + Vue 的宠物领养管理系统
前端·vue.js·python·毕业设计·fastapi·宠物
一杯奶茶¥14 小时前
水果销售网站 CRM客户信息管理系统 超市管理系 酒店管理系统 健身房管理系统 在线音乐网站 校园招聘系统
java·vue.js·spring boot·mysql·spring·java项目
lichenyang45314 小时前
鸿蒙 Web 容器(五·完结):闭环回传、容器治理,兼谈 AtomicServiceEnhancedWeb
前端
lichenyang45314 小时前
鸿蒙 Web 容器(四):ArkTS 拿到请求后,怎么「按 action 找能力」?
前端
lichenyang45314 小时前
鸿蒙 Web 容器(三):H5 怎么「调」到 ArkTS?
前端
代码不加糖14 小时前
Proxy能够监听到对象中的对象的引用吗?
开发语言·前端·javascript
英勇无比的消炎药14 小时前
一站式搞定品牌风格:TinyRobot 主题定制从入门到精通
vue.js
光影少年14 小时前
react 原理与进阶
前端·react.js·掘金·金石计划
kyrie2814 小时前
Vue 全套性能优化方案
前端