前端八股5---组件通信

目录

  • 一、通信方式概览
  • 二、详细说明
  • [1. Props(父传子)](#1. Props(父传子))
  • [2. Props + 函数(子传父)](#2. Props + 函数(子传父))
  • [3. 自定义事件(emit)- 子传父](#3. 自定义事件(emit)- 子传父)
  • Q1:自定义事件和原生事件的区别?
  • [Q2:defineEmits 的作用是什么?](#Q2:defineEmits 的作用是什么?)
  • [Q3:emit 可以传多个参数吗?](#Q3:emit 可以传多个参数吗?)
  • Q4:子组件如何修改父组件的数据?
  • [4. v-model(父子双向绑定)](#4. v-model(父子双向绑定))
  • [5. provide / inject(祖孙通信)](#5. provide / inject(祖孙通信))
  • [6. mitt / eventBus(任意组件通信)](#6. mitt / eventBus(任意组件通信))
  • [7. Pinia / Vuex(全局状态管理)](#7. Pinia / Vuex(全局状态管理))
  • [8. refs / parent(直接访问)](#8. refs / parent(直接访问))
  • [9. slot 插槽(父传模板)](#9. slot 插槽(父传模板))
  • [10. attrs / listeners(属性透传)](#10. attrs / listeners(属性透传))
  • 三、选择建议
  • 四、面试速记卡片

一、通信方式概览

方式 方向 适用场景 复杂度
Props 父→子 最基础,父传数据给子
Props + 函数 子→父 子组件需要通知父组件 ⭐⭐
自定义事件 (emit) 子→父 子组件向父组件传数据 ⭐⭐
v-model 双向 父子组件双向绑定 ⭐⭐
provide/inject 祖→孙 跨多级组件传递数据 ⭐⭐⭐
mitt / eventBus 任意 任意组件间通信 ⭐⭐⭐
Pinia / Vuex 任意 大型应用全局状态管理 ⭐⭐⭐⭐
refs / parent 直接访问 直接调用子/父组件方法 ⭐⭐
slot 插槽 父→子 父组件传递模板内容 ⭐⭐
attrs / listeners 祖→孙 透传属性和事件 ⭐⭐⭐

二、详细说明

1. Props(父传子)

最基础的通信方式,父组件通过属性向子组件传递数据。

  • 父组件在调用子组件的地方绑定数据,
  • 子组件通过 defineProps 接收父组件传来的数据。
javascript 复制代码
<!-- 父组件 Father.vue -->
<template>
  <Child :name="userName" :age="18" />
</template>

<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const userName = ref('张三')
</script>

<!-- 子组件 Child.vue -->
<script setup>
// 方式1:声明接收
const props = defineProps(['name', 'age'])

// 方式2:带类型声明
const props = defineProps({
  name: String,
  age: {
    type: Number,
    default: 0,
    required: true
  }
})

// 使用 props.name
console.log(props.name)
</script>

注意事项:

  • Props 是只读的,子组件不能直接修改

  • 如果需要修改,应该通过 emit 通知父组件修改


2. Props + 函数(子传父)

父组件传递一个函数给子组件,子组件调用该函数并传参,实现子传父。

  • 父组件在调用子组件的地方传入一个函数,
  • 子组件通过 defineProps 接收,
  • 子组件调用这个方法并传参 ,把数据传回给父组件。
javascript 复制代码
<!-- 父组件 -->
<template>
  <Child :onSendMessage="handleMessage" />
</template>

<script setup>
import Child from './Child.vue'

function handleMessage(msg) {
  console.log('收到子组件的消息:', msg)
}
</script>

<!-- 子组件 -->
<script setup>
const props = defineProps(['onSendMessage'])

function sendToParent() {
  props.onSendMessage('Hello 父组件')
}
</script>

3. 自定义事件(emit)- 子传父

Vue 推荐的标准子传父方式,子组件通过 $emit 触发事件,父组件监听事件。

  • 父:给子组件绑定事件名,用@自定义事件名
  • 子:用defineEmits 声明事件,用 emit(名字, 数据) 触发
  • 父:自动收到数据
javascript 复制代码
<!-- 父组件 -->
<template>
  <Child @send-message="handleMessage" />
</template>

<script setup>
import Child from './Child.vue'

function handleMessage(msg) {
  console.log('收到消息:', msg)
}
</script>

<!-- 子组件 -->
<script setup>
// 声明事件
const emit = defineEmits(['send-message'])

function sendToParent() {
  // 触发事件,传递数据
  emit('send-message', 'Hello 父组件')
}
</script>

Q1:自定义事件和原生事件的区别?

对比 原生事件 自定义事件
事件名 固定(click、input等) 任意名称
触发方式 用户交互自动触发 手动调用 emit 触发
$event 对象 DOM 事件对象 emit 传递的数据
使用场景 监听 DOM 操作 组件间通信

Q2:defineEmits 的作用是什么?

defineEmits 是 Vue 3 中用于声明自定义事件的函数。它有两个作用:

  1. 类型声明:让 TypeScript 知道有哪些事件

  2. 运行时验证:可以验证事件参数格式(开发环境)

  3. 返回值:返回 emit 函数,用于触发事件

Q3:emit 可以传多个参数吗?

可以,但只推荐传一个参数。因为:

  • 如果传多个,只有第一个参数会作为 $event 传递

  • 其他参数会被忽略,导致数据丢失

  • 推荐用对象或数组包裹多个值

javascript 复制代码
// ❌ 不推荐
emit('update', name, age, email)

// ✅ 推荐
emit('update', { name, age, email })

Q4:子组件如何修改父组件的数据?

Vue 是单向数据流,子组件不能直接修改父组件的数据。正确做法是:

  1. 子组件通过 emit 触发事件,把要修改的值传出去

  2. 父组件监听事件,在回调中修改自己的数据

  3. 修改后的数据通过 props 传回子组件


4. v-model(父子双向绑定)

v-model 是双向绑定的语法糖,本质是 props + emit。

javascript 复制代码
<!-- 父组件 -->
<template>
  <!-- 写法1:v-model -->
  <CustomInput v-model="username" />
  
  <!-- 写法2:完整写法(v-model 的本质) -->
  <CustomInput 
    :modelValue="username" 
    @update:modelValue="username = $event" 
  />
  
  <!-- 写法3:多个 v-model 绑定 -->
  <CustomForm 
    v-model:firstName="firstName"
    v-model:lastName="lastName"
  />
</template>

<script setup>
import { ref } from 'vue'
const username = ref('')
const firstName = ref('')
const lastName = ref('')
</script>

<!-- 子组件 CustomInput.vue -->
<template>
  <input 
    type="text"
    :value="modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<!-- 支持多个 v-model 的子组件 -->
<template>
  <input :value="firstName" @input="emit('update:firstName', $event.target.value)" />
  <input :value="lastName" @input="emit('update:lastName', $event.target.value)" />
</template>

<script setup>
defineProps(['firstName', 'lastName'])
const emit = defineEmits(['update:firstName', 'update:lastName'])
</script>

5. provide / inject(祖孙通信)

解决跨多级组件通信问题,祖先组件提供数据,后代组件注入使用。

javascript 复制代码
<!-- 祖先组件 GrandParent.vue -->
<script setup>
import { provide, ref, reactive } from 'vue'

// 提供普通数据
const message = ref('Hello')
provide('message', message)

// 提供响应式数据和方法
const count = ref(0)
function increment() {
  count.value++
}
provide('countContext', { count, increment })
</script>

<!-- 中间组件 - 不需要任何处理,透传即可 -->

<!-- 孙组件 GrandChild.vue -->
<script setup>
import { inject } from 'vue'

// 注入数据
const message = inject('message')
const { count, increment } = inject('countContext')

// 也可以提供默认值
const optional = inject('notExists', '默认值')

// 使用函数式默认值
const data = inject('key', () => ({ name: '默认' }))
</script>

注意事项:

  • provide 的数据建议使用 refreactive 保持响应式

  • inject 的数据在子组件中可以直接使用

  • 适合祖孙通信,中间组件不需要知道数据的存在


6. mitt / eventBus(任意组件通信)

轻量级的事件总线,适合中小型项目的任意组件通信。

bash

javascript 复制代码
npm i mitt

typescript

javascript 复制代码
// utils/emitter.ts
import mitt from 'mitt'
const emitter = mitt()
export default emitter

vue

javascript 复制代码
<!-- 发送数据组件 Sender.vue -->
<script setup>
import emitter from '@/utils/emitter'
import { ref } from 'vue'

const message = ref('Hello')

function sendMessage() {
  emitter.emit('custom-event', message.value)
}
</script>

<!-- 接收数据组件 Receiver.vue -->
<script setup>
import emitter from '@/utils/emitter'
import { onUnmounted } from 'vue'

function handleMessage(data) {
  console.log('收到消息:', data)
}

// 监听事件
emitter.on('custom-event', handleMessage)

// 必须解绑,防止内存泄漏!
onUnmounted(() => {
  emitter.off('custom-event', handleMessage)
})
</script>

<!-- 清除所有事件 -->
emitter.all.clear()

mitt API 速查:

方法 作用
emitter.on(event, handler) 监听事件
emitter.off(event, handler) 解绑事件(必须指定 handler)
emitter.emit(event, data) 触发事件
emitter.all.clear() 清除所有事件

7. Pinia / Vuex(全局状态管理)

官方推荐的状态管理方案,适合大型应用。

bash

javascript 复制代码
npm i pinia

typescript

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

export const useUserStore = defineStore('user', {
  // state:存放数据
  state: () => ({
    name: '张三',
    age: 18,
    token: ''
  }),
  
  // getters:计算属性
  getters: {
    doubleAge: (state) => state.age * 2,
    userInfo: (state) => `${state.name} - ${state.age}岁`
  },
  
  // actions:业务逻辑(同步/异步)
  actions: {
    // 同步修改
    setName(name: string) {
      this.name = name
    },
    
    // 异步修改
    async login(username: string, password: string) {
      const res = await api.login(username, password)
      this.token = res.data.token
      this.name = res.data.name
    },
    
    // 批量修改
    updateUser(data: Partial<{ name: string; age: number }>) {
      Object.assign(this.$state, data)
    }
  }
})

vue

javascript 复制代码
<!-- 组件中使用 -->
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构 state 需要 storeToRefs 保持响应式
const { name, age, doubleAge } = storeToRefs(userStore)

// actions 可以直接解构
const { setName, login } = userStore

// 使用
function updateUser() {
  setName('李四')
  // 或
  userStore.name = '王五'
}
</script>

<template>
  <div>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
    <p>两倍年龄:{{ doubleAge }}</p>
    <button @click="setName('新名字')">修改</button>
  </div>
</template>

Pinia 核心三概念:

概念 作用 类比
state 存放共享数据 组件中的 data
getters 计算/过滤数据 组件中的 computed
actions 业务逻辑(同步+异步) 组件中的 methods

8. refs / parent(直接访问)

直接获取组件实例,调用其方法或访问其数据。

vue

javascript 复制代码
<!-- 父组件 -->
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

// 获取子组件实例
const childRef = ref(null)

function callChildMethod() {
  // 调用子组件的方法
  childRef.value.sayHello()
  // 访问子组件的数据
  console.log(childRef.value.childData)
}

// 访问父组件
function getParent() {
  console.log(childRef.value.$parent)
}
</script>

<!-- 子组件 -->
<script setup>
import { ref } from 'vue'

const childData = ref('子组件数据')

function sayHello() {
  console.log('Hello from child')
}

// 暴露给父组件(<script setup> 默认不暴露)
defineExpose({
  childData,
  sayHello
})
</script>

9. slot 插槽(父传模板)

父组件向子组件传递模板内容。

vue

javascript 复制代码
<!-- 父组件 -->
<template>
  <Card>
    <!-- 默认插槽 -->
    <p>这是卡片内容</p>
    
    <!-- 具名插槽 -->
    <template #header>
      <h2>卡片标题</h2>
    </template>
    
    <template #footer>
      <button>确定</button>
    </template>
    
    <!-- 作用域插槽:子组件向父组件传递数据 -->
    <template #item="{ item, index }">
      <div>{{ index }} - {{ item.name }}</div>
    </template>
  </Card>
</template>

<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <div class="header">
      <slot name="header">默认标题</slot>
    </div>
    <div class="content">
      <slot>默认内容</slot>
    </div>
    <div class="footer">
      <slot name="footer">默认底部</slot>
    </div>
    
    <!-- 作用域插槽:传递数据给父组件 -->
    <div v-for="(item, index) in items" :key="index">
      <slot name="item" :item="item" :index="index"></slot>
    </div>
  </div>
</template>

10. attrs / listeners(属性透传)

自动透传父组件传入但子组件未声明的属性和事件。

vue

javascript 复制代码
<!-- 父组件 -->
<template>
  <BaseButton 
    class="big-btn" 
    :disabled="isDisabled"
    @click="handleClick"
    data-id="123"
  >
    按钮
  </BaseButton>
</template>

<!-- 子组件 BaseButton.vue -->
<template>
  <!-- $attrs 会自动绑定到根元素 -->
  <button :class="$attrs.class" v-bind="$attrs">
    <slot />
  </button>
</template>

<script setup>
// 如果声明了 props,对应的属性就不会进入 $attrs
// const props = defineProps(['disabled'])

// 获取所有透传属性
const attrs = useAttrs()
console.log(attrs) // { class: 'big-btn', onClick: fn, dataId: '123' }

// 禁用自动透传(Vue 3.3+)
defineOptions({
  inheritAttrs: false
})
</script>

三、选择建议

场景 推荐方案
父子组件简单传值 Props / emit
父子组件双向绑定 v-model
祖孙组件跨级传递 provide/inject
任意组件通信(小型项目) mitt
任意组件通信(大型项目) Pinia
需要访问子组件实例 $refs
父组件传递模板给子组件 slot
封装高阶组件/透传属性 $attrs

四、面试速记卡片

text

复制代码
父子通信:
├── Props(父→子)
├── emit(子→父)
├── v-model(双向)
└── $refs(直接访问)

跨级通信:
├── provide/inject(祖→孙)
└── $attrs(属性透传)

全局通信:
├── mitt(轻量级事件总线)
└── Pinia/Vuex(状态管理)

模板传递:
└── slot(插槽)
相关推荐
Daemon2 小时前
AI Agent系列记录(第二篇)
前端·人工智能·后端
JianZhen✓2 小时前
2026——Cursor全攻略+AI编程/前端辅助工具汇总(含问题速解)
前端·ai编程
vmiao2 小时前
【JS进阶】模拟正确处理并渲染后台数据
前端·javascript
小彭努力中2 小时前
204.Vue3 + OpenLayers:加载 GIF 文件(CSS 背景实现动画标记)
前端·css·vue·openlayers·geojson·webgis
Wect2 小时前
JS手撕:函数进阶 & 设计模式解析
前端·javascript·面试
chQHk57BN2 小时前
前端状态管理:Redux、Vuex、Pinia哪个更适合你?
前端
kyriewen112 小时前
每日知识点:this 指向之谜——是谁在 call 我?
前端·javascript·vue.js·前端框架·ecmascript·jquery·html5
浩星2 小时前
electron系列6之性能优化:从启动慢到内存泄漏
前端·javascript·electron
飞Link2 小时前
pprint 全量技术手册:复杂数据结构的结构化输出引擎
开发语言·前端·python