目录

前言
一、父传子
1.1 基础:传递简单数据(字符串 / 布尔 / 数字)
父组件:绑定属性
子组件:defineProps 接收 + 校验
1.2 进阶:传递复杂数据(对象 / 数组)
父组件:传递对象
子组件:接收 + 监听更新
1.3 父传子核心注意事项
二、子传父
2.1 基础:defineEmits 触发事件传值
子组件:定义事件 + 触发传值
父组件:监听事件 + 接收数据
2.2 进阶:v-model 简化双向绑定
Vue3 v-model 核心协议
实战:v-model 控制弹框
子组件适配
父组件简化绑定
扩展:多 v-model 绑定
2.3 特殊:defineExpose 暴露方法
子组件:暴露方法
父组件:引用并调用
三、兄弟组件传值
3.1 父组件中转(✅ 使用最频繁)
示例结构
父组件(中转)
子组件 A(发出事件)
子组件 B(接收数据)
3.2 Pinia 状态管理(⚙️ 中大型项目常用)
① 安装依赖
② 全局注册
③ 定义仓库
④ 子组件 A(更新状态)
⑤ 子组件 B(共享状态)
3.3 Mitt 事件总线(📡 临时通信用)
① 安装依赖
② 创建事件总线
③ 子组件 A(发送事件)
④ 子组件 B(监听事件)
3.4 兄弟通信总结与最佳实践
四、常见误区与最佳实践
4.1 避坑指南
4.2 最佳实践
五、本文总结

前言

Vue3 基于 <script setup> 语法的父子通信,核心围绕 defineProps(父传子)defineEmits(子传父)v-model(双向绑定) 和 defineExpose(子组件暴露方法),相比 Vue2 更简洁、类型友好。本文提炼实战核心,聚焦 “关键用法 + 极简示例 + 避坑点”,帮助快速掌握核心逻辑。

一、父传子

父传子:defineProps 核心用法

核心逻辑:父组件通过「属性绑定」传值,子组件通过 defineProps 接收并校验,支持简单数据、复杂数据传递。

1.1 基础:传递简单数据(字符串 / 布尔 / 数字)

父组件:绑定属性

vue

<template>
  <!-- 绑定 title(字符串)、isDisabled(布尔) -->
  <ChildComp :title="dialogTitle" :is-disabled="isLoading" />
</template>

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

const dialogTitle = ref('新增立项'); // 要传递的字符串
const isLoading = ref(false); // 要传递的布尔值
</script>

组件懒加载:

import { ref, reactive, defineAsyncComponent } from 'vue'

// 懒加载弹框组件
// 只有当组件被需要时(dialogVisible为true)才会加载
const ChildComp = defineAsyncComponent(() => import('./ChildComp.vue'))
子组件:defineProps 接收 + 校验

vue

<template>
  <div>
    <h3>{{ title }}</h3>
    <button :disabled="isDisabled">提交</button>
  </div>
</template>

<script setup>
// 接收并校验父组件数据(类型、必填、默认值)
const props = defineProps({
  title: {
    type: String, // 明确类型
    required: true, // 必填(缺省会警告)
    validator: (val) => val.length >= 2 // 自定义验证(标题长度≥2)
  },
  isDisabled: {
    type: Boolean,
    required: false,
    default: false // 简单类型默认值直接写
  }
});
</script>

1.2 进阶:传递复杂数据(对象 / 数组)

复杂数据需注意「响应式监听」和「避免引用污染」。

父组件:传递对象

vue

<template>
  <ChildComp :form-init="currentForm" />
</template>

<script setup>
import { ref } from 'vue';
const currentForm = ref({ name: '企业采购', value: 500000 }); // 复杂对象
</script>
子组件:接收 + 监听更新

vue

<template>
  <el-input v-model="localForm.name" />
</template>

<script setup>
import { ref, reactive, watch } from 'vue';

const props = defineProps({
  formInit: {
    type: Object,
    required: false,
    default: () => ({}) // 复杂类型默认值:必须用函数返回(避免共享引用)
  }
});

// 子组件本地数据(不直接改 props)
const localForm = reactive({});

// 监听父组件数据变化,同步到本地
watch(
  () => props.formInit,
  (newVal) => {
    Object.assign(localForm, newVal); // 合并更新,保持响应式
  },
  { immediate: true } // 初始渲染立即执行
);
</script>

1.3 父传子核心注意事项

  1. 复杂类型(Object/Array)默认值必须用 () => 值 函数返回,避免多组件共享引用;
  2. 子组件禁止直接修改 props(会触发警告),需同步到本地数据修改;
  3. 属性名:父组件用 kebab-case(如 is-disabled),子组件用 camelCase(如 isDisabled),Vue 自动映射。


二、子传父

子传父:3 种核心方式

核心逻辑:子组件通过「事件触发」传值,父组件「监听事件」接收;v-model 简化双向绑定,defineExpose 支持父组件主动调用子方法。

2.1 基础defineEmits 触发事件传值

子组件:定义事件 + 触发传值

vue

<template>
  <button @click="handleSubmit">提交</button>
</template>

<script setup>
import { reactive } from 'vue';

// 1. 定义要触发的事件(支持多个)
const emit = defineEmits(['close', 'submit']);

// 子组件本地数据
const formData = reactive({ name: '', value: null });

// 2. 触发事件:emit('事件名', 要传递的数据)
const handleSubmit = () => {
  emit('submit', { ...formData }); // 解构传递(避免父组件改子组件引用)
  emit('close'); // 无参数事件
};
</script>
父组件:监听事件 + 接收数据

vue

<template>
  <ChildComp @submit="onSubmit" @close="onClose" />
</template>

<script setup>
const onSubmit = (childData) => {
  console.log('子组件传递的表单数据:', childData); // 接收子组件数据
  // 调用接口等业务逻辑...
};

const onClose = () => {
  console.log('子组件触发关闭');
};
</script>

2.2 进阶v-model 简化双向绑定

适用于「子组件控制父组件状态」(如弹框显示 / 隐藏),替代 “:状态+@update” 的繁琐写法。

Vue3 v-model 核心协议
  • 父组件:<Child v-model:状态名="父状态" />
  • 子组件:接收 状态名 → 触发 update:状态名 事件传新值
实战:v-model 控制弹框
子组件适配

vue

<template>
  <el-dialog :visible="visible" @close="handleClose">内容</el-dialog>
</template>

<script setup>
// 1. 接收父组件 v-model 传递的状态(visible)
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  }
});

// 2. 定义 emit,需包含 update:visible 事件
const emit = defineEmits(['update:visible']);

// 3. 关闭时触发事件,通知父组件更新状态
const handleClose = () => {
  emit('update:visible', false); // 传递新值(false)
};
</script>
父组件简化绑定

vue

<template>
  <!-- v-model:visible 直接绑定状态,无需额外监听 close 事件 -->
  <ChildComp v-model:visible="dialogVisible" />
  <button @click="dialogVisible = true">打开弹框</button>
</template>

<script setup>
import { ref } from 'vue';
const dialogVisible = ref(false);
</script>
扩展:多 v-model 绑定

父组件可同时绑定多个子组件状态:

vue

<!-- 父组件:绑定 visible 和 submitLoading 两个状态 -->
<ChildComp 
  v-model:visible="dialogVisible" 
  v-model:submit-loading="isLoading" 
/>

2.3 特殊:defineExpose 暴露方法

适用于「父组件主动调用子组件方法」(如强制清空表单、触发验证)。

子组件:暴露方法

vue

<script setup>
import { ref } from 'vue';

// 子组件内部方法
const resetForm = () => {
  console.log('清空表单');
  // 实际清空逻辑...
};

// 暴露方法(父组件可通过 ref 调用)
defineExpose({ resetForm });
</script>
父组件:引用并调用

vue

<template>
  <ChildComp ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue';

// 1. 给子组件加 ref 标识
const childRef = ref();

// 2. 调用子组件暴露的方法
const callChildMethod = () => {
  childRef.value?.resetForm(); // 可选链避免报错
};
</script>


三、兄弟组件传值

兄弟组件传值(按使用频率排序)

在 Vue3 中,兄弟组件(非父子关系)之间不能直接使用 propsemit 传值。
常见的通信方式主要有 父组件中转 → Pinia 状态共享 → Mitt 事件总线 三种。
以下按照 实际使用频率 从高到低讲解。

3.1 父组件中转(✅ 使用最频繁)

最推荐方式,逻辑清晰、类型安全、无依赖。
核心思路:通过父组件作为“桥梁”,子 A 发事件 → 父组件接收 → 再传给子 B。

示例结构

Parent
 ├─ ChildA(发出数据)
 └─ ChildB(接收数据)

父组件(中转)
<template>
  <div>
    <ChildA @select-item="handleSelect" />
    <ChildB :selected-item="selectedItem" />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import ChildA from './ChildA.vue';
import ChildB from './ChildB.vue';

const selectedItem = ref(null);
const handleSelect = (item) => {
  selectedItem.value = item;
};
</script>
子组件 A(发出事件)
<template>
  <el-button @click="chooseItem('项目管理')">选择项目管理</el-button>
</template>

<script setup>
const emit = defineEmits(['select-item']);
const chooseItem = (val) => emit('select-item', val);
</script>
子组件 B(接收数据)
<template>
  <div>当前选择:{{ selectedItem }}</div>
</template>

<script setup>
const props = defineProps({
  selectedItem: String
});
</script>

优点

  • 数据流清晰;

  • 无需额外依赖;

  • 适用于多数业务组件间通信。

⚠️ 注意

  • 若兄弟层级过深或数量多,父组件容易成为“中转站混乱点”,可用 Pinia 优化。


3.2 Pinia 状态管理(⚙️ 中大型项目常用)

首先,Pinia 不是 Vue3 默认内置的,需要单独安装依赖并在项目中注册。它是Vue3 官方推荐的全局状态库,适用于复杂通信、多页面共享数据场景。

① 安装依赖

在项目根目录运行以下命令(确保 Node 环境已安装):

npm install pinia

如果你使用 Vue CLI 或 Vite 创建的项目,这一步只需要执行一次。

② 全局注册

在项目中注册 Pinia(main.js):必须在 Vue 应用入口文件中进行注册,才能在各组件中正常使用

// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount('#app');
③ 定义仓库
// stores/useProjectStore.js
import { defineStore } from 'pinia';

export const useProjectStore = defineStore('project', {
  state: () => ({
    selectedItem: ''
  }),
  actions: {
    setItem(val) {
      this.selectedItem = val;
    }
  }
});
④ 子组件 A(更新状态)
<script setup>
import { useProjectStore } from '@/stores/useProjectStore';
const store = useProjectStore();

const handleSelect = (val) => {
  store.setItem(val);
};
</script>

<template>
  <el-button @click="handleSelect('系统管理')">选择系统管理</el-button>
</template>
⑤ 子组件 B(共享状态)
<script setup>
import { useProjectStore } from '@/stores/useProjectStore';
const store = useProjectStore();
</script>

<template>
  <div>当前选择:{{ store.selectedItem }}</div>
</template>

优点

  • 状态集中管理;

  • 响应式共享;

  • 模块化、可调试。

⚠️ 注意

  • 不要滥用全局状态,应仅在确实需要共享的核心数据中使用;

  • 在大型项目中,建议模块化拆分 store。

3.3 Mitt 事件总线(📡 临时通信用)

Vue3 中已移除 $emit/$on 全局事件总线机制,
但可用轻量库 Mitt 实现类似功能。

① 安装依赖

npm install mitt

② 创建事件总线
// utils/eventBus.js
import mitt from 'mitt';
export const eventBus = mitt();
③ 子组件 A(发送事件)
<script setup>
import { eventBus } from '@/utils/eventBus';
const sendMsg = () => eventBus.emit('updateItem', '合同管理');
</script>

<template>
  <el-button @click="sendMsg">发送消息</el-button>
</template>
④ 子组件 B(监听事件)
<script setup>
import { eventBus } from '@/utils/eventBus';
import { ref, onMounted, onBeforeUnmount } from 'vue';

const item = ref('');
onMounted(() => {
  eventBus.on('updateItem', (val) => (item.value = val));
});
onBeforeUnmount(() => {
  eventBus.off('updateItem'); // 组件销毁时清理监听
});
</script>

<template>
  <div>接收到:{{ item }}</div>
</template>

优点

  • 实现简单;

  • 组件层级无关;

  • 适合临时广播、消息提示。

⚠️ 注意

  • 必须在组件卸载时 off() 移除监听;

  • 不推荐长期持有全局状态;

  • 建议仅限小项目或独立模块使用。

3.4 兄弟通信总结与最佳实践

使用方式适用场景优点注意事项
父组件中转普通兄弟组件通信简单直观、零依赖层级过多会臃肿
Pinia 状态共享多模块、大型项目状态集中、响应式store 拆分与命名管理
Mitt 事件总线临时广播、解耦通信轻量灵活需手动清理事件监听

🧩 推荐选型:

  1. 90% 场景 → 父组件中转;

  2. 多模块共享状态 → Pinia;

  3. 临时事件广播 → Mitt。


四、常见误区与最佳实践

4.1 避坑指南

  1. 禁止子组件直接改 props:必须通过 emit 通知父组件修改;
  2. 复杂类型默认值错写:Object/Array 需用 () => 值,否则多组件共享数据;
  3. v-model 事件名错写:子组件必须触发 update:状态名(如 update:visible),而非自定义名;
  4. 子组件方法未暴露<script setup> 中方法默认私有,需 defineExpose 才能被父组件调用。

4.2 最佳实践

  1. 优先用 props + emit:低耦合,覆盖 90% 场景;
  2. 状态双向同步用 v-model:简化弹框、输入框等场景代码;
  3. 复杂数据传递用「浅拷贝」:子传父时用 { ...data },避免引用污染;
  4. 明确类型校验:defineProps 写清 type 和 required,减少运行时错误。


五、本文总结

Vue3 父子通信核心是「API 简洁化 + 职责清晰化」:

  1. 父传子defineProps 接收 + 校验,复杂数据用 watch 同步;
  2. 子传父:基础用 defineEmits 触发事件,双向状态用 v-model,主动调用用 defineExpose
  3. 核心原则:不直接改 props、明确类型、减少耦合,适配中后台系统高频场景(如弹框、表单)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐