Vue3 子传父全解析:从基础用法到实战避坑

31 阅读8分钟

在 Vue3 开发中,组件通信是绕不开的核心场景,而子传父作为最基础、最常用的通信方式之一,更是新手入门必掌握的知识点。不同于 Vue2 的 $emit 写法,Vue3 组合式 API(<script setup>)简化了子传父的实现逻辑,但也有不少细节和进阶技巧需要注意。

本文将抛开 TypeScript,用最通俗的语言 + 可直接复制的实战代码,从基础用法、进阶技巧、常见场景到避坑指南,全方位讲解 Vue3 子传父,新手看完就能上手,老手也能查漏补缺。

一、核心原理:子组件触发事件,父组件监听事件

Vue3 子传父的核心逻辑和 Vue2 一致:子组件通过触发自定义事件,将数据传递给父组件;父组件通过监听该自定义事件,接收子组件传递的数据

关键区别在于:Vue3 <script setup> 中,无需通过 this.$emit 触发事件,而是通过 defineEmits 声明事件后,直接调用 emit 函数即可,语法更简洁、更直观。

先记住核心流程,再看具体实现:

  1. 子组件:用 defineEmits 声明要触发的自定义事件(可选但推荐);
  2. 子组件:在需要传值的地方(如点击事件、接口回调),调用 emit('事件名', 要传递的数据)
  3. 父组件:在使用子组件的地方,通过 @事件名="处理函数" 监听事件;
  4. 父组件:在处理函数中,接收子组件传递的数据并使用。

二、基础用法:最简洁的子传父实现(必学)

我们用一个「子组件输入内容,父组件实时显示」的简单案例,讲解基础用法,代码可直接复制到项目中运行。

1. 子组件(Child.vue):声明事件 + 触发事件

<template>
  <div class="child">
    <h4>我是子组件</h4>
    <!-- 输入框输入内容,触发input事件,传递输入值 -->
    <input 
      type="text" 
      v-model="childInput" 
      @input="handleInput"
      placeholder="请输入要传递给父组件的内容"
    />
    <!-- 按钮点击,传递固定数据 -->
    <button @click="handleClick" style="margin-top: 10px;">
      点击向父组件传值
    </button>
  </div>
</template>

<script setup>
// 1. 声明要触发的自定义事件(数组形式,元素是事件名)
// 可选,但推荐声明:增强代码可读性,IDE会有语法提示,避免拼写错误
const emit = defineEmits(['inputChange', 'btnClick'])

// 子组件内部数据
const childInput = ref('')

// 输入框变化时,触发事件并传递输入值
const handleInput = () => {
  // 2. 触发事件:第一个参数是事件名,第二个参数是要传递的数据(可选,可多个)
  emit('inputChange', childInput.value)
}

// 按钮点击时,触发事件并传递固定对象
const handleClick = () => {
  emit('btnClick', {
    name: '子组件',
    msg: '这是子组件通过点击按钮传递的数据'
  })
}
</script>

2. 父组件(Parent.vue):监听事件 + 接收数据

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <p>子组件输入的内容:{{ parentMsg }}</p>
    <p>子组件点击传递的数据:{{ parentData }}</p>
    
    <!-- 3. 监听子组件声明的自定义事件,绑定处理函数 -->
    <Child 
      @inputChange="handleInputChange"
      @btnClick="handleBtnClick"
    />
  </div>
</template>

<script setup>
// 引入子组件
import Child from './Child.vue'
import { ref, reactive } from 'vue'

// 父组件接收数据的容器
const parentMsg = ref('')
const parentData = reactive({
  name: '',
  msg: ''
})

// 4. 处理子组件触发的inputChange事件,接收传递的数据
const handleInputChange = (val) => {
  // val 就是子组件emit传递过来的值(childInput.value)
  parentMsg.value = val
}

// 处理子组件触发的btnClick事件,接收传递的对象
const handleBtnClick = (data) => {
  // data 是子组件传递的对象,直接解构或赋值即可
  parentData.name = data.name
  parentData.msg = data.msg
}
</script>

3. 核心细节说明

  • defineEmits 是 Vue3 内置的宏,无需导入,可直接使用;
  • emit 函数的第一个参数必须和 defineEmits 中声明的事件名一致(大小写敏感),否则父组件无法监听到;
  • emit 可传递多个参数,比如 emit('event', val1, val2),父组件处理函数可对应接收 (val1, val2) => {}
  • 父组件监听事件时,可使用 @事件名(简写)或 v-on:事件名(完整写法),效果一致。

三、进阶用法:优化子传父的体验(实战常用)

基础用法能满足简单场景,但在实际开发中,我们还会遇到「事件校验」「双向绑定」「事件命名规范」等需求,这部分进阶技巧能让你的代码更规范、更健壮。

1. 事件校验:限制子组件传递的数据类型

通过 defineEmits 的对象形式,可对事件传递的数据进行类型校验,避免子组件传递错误类型的数据,提升代码可靠性(类似 props 校验)。

<script setup>
// 对象形式声明事件,key是事件名,value是校验函数(参数是子组件传递的数据,返回boolean)
const emit = defineEmits({
  // 校验inputChange事件传递的数据必须是字符串
  inputChange: (val) => {
    return typeof val === 'string'
  },
  // 校验btnClick事件传递的数据必须是对象,且包含name和msg属性
  btnClick: (data) => {
    return typeof data === 'object' && 'name' in data && 'msg' in data
  }
})

// 若传递的数据不符合校验,控制台会报警告(不影响代码运行,仅提示)
const handleInput = () => {
  emit('inputChange', 123) // 传递数字,不符合校验,控制台报警告
}
</script>

2. 双向绑定:v-model 简化子传父(高频场景)

很多时候,子传父是为了「修改父组件的数据」,比如表单组件、开关组件,这时可使用 v-model 简化代码,实现父子组件双向绑定,无需手动声明事件和处理函数。

Vue3 中,v-model 本质是「语法糖」,等价于 :modelValue="xxx" @update:modelValue="xxx = $event"

优化案例:子组件开关,父组件显示状态

<!-- 子组件(Child.vue) -->
<template>
  <div class="child">
    <h4>子组件开关</h4>
    <button @click="handleSwitch">
      {{ isOpen ? '关闭' : '打开' }}
    </button>
  </div>
</template>

<script setup>
// 1. 接收父组件通过v-model传递的modelValue
const props = defineProps(['modelValue'])
// 2. 声明update:modelValue事件(固定命名,不可修改)
const emit = defineEmits(['update:modelValue'])

// 子组件内部使用父组件传递的值
const isOpen = computed(() => props.modelValue)

// 开关切换,触发事件,修改父组件数据
const handleSwitch = () => {
  emit('update:modelValue', !isOpen.value)
}
</script>
<!-- 父组件(Parent.vue) -->
<template>
  <div class="parent">
    <h3>父组件:{{ isSwitchOpen ? '开关已打开' : '开关已关闭' }}</h3>
    <!-- 直接使用v-model,无需手动监听事件 -->
    <Child v-model="isSwitchOpen" />
  </div>
</template>

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

const isSwitchOpen = ref(false)
</script>

扩展:多个 v-model 双向绑定

Vue3 支持给同一个子组件绑定多个 v-model,只需给 v-model 加后缀,对应子组件的propsemit 即可。

<!-- 父组件 -->
<Child 
  v-model:name="parentName" 
  v-model:age="parentAge" 
/>

<!-- 子组件 -->
<script setup>
// 接收多个v-model传递的props
const props = defineProps(['name', 'age'])
// 声明对应的update事件
const emit = defineEmits(['update:name', 'update:age'])

// 触发事件修改父组件数据
emit('update:name', '新名字')
emit('update:age', 25)
</script>

3. 事件命名规范:提升代码可读性

在实际开发中,遵循统一的事件命名规范,能让团队协作更高效,推荐以下规范:

  • 事件名采用「kebab-case 短横线命名」(和 HTML 事件命名一致),比如 input-change 而非 inputChange
  • 事件名要语义化,体现事件的用途,比如 form-submit(表单提交)、delete-click(删除点击);
  • 双向绑定的事件固定为 update:xxx,xxx 对应 props 名,比如 update:nameupdate:visible

四、实战场景:子传父的常见应用

结合实际开发中的高频场景,给大家补充 3 个常用案例,覆盖大部分子传父需求。

场景1:子组件表单提交,父组件接收表单数据

<!-- 子组件(FormChild.vue) -->
<template>
  <div class="form-child">
    <input v-model="form.name" placeholder="请输入姓名" />
    <input v-model="form.age" type="number" placeholder="请输入年龄" />
    <button @click="handleSubmit">提交表单</button>
  </div>
</template>

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

const emit = defineEmits(['form-submit'])

const form = reactive({
  name: '',
  age: ''
})

const handleSubmit = () => {
  // 表单校验(简化)
  if (!form.name || !form.age) return alert('请填写完整信息')
  // 提交表单数据给父组件
  emit('form-submit', form)
  // 提交后重置表单
  form.name = ''
  form.age = ''
}
</script>

场景2:子组件关闭弹窗,父组件控制弹窗显示/隐藏

<!-- 子组件(ModalChild.vue) -->
<template>
  <div class="modal" v-if="visible">
    <div class="modal-content">
      <h4>子组件弹窗</h4>
      <button @click="handleClose">关闭弹窗</button>
    </div>
  </div>
</template>

<script setup>
const props = defineProps(['visible'])
const emit = defineEmits(['close-modal'])

const handleClose = () => {
  // 触发关闭事件,通知父组件隐藏弹窗
  emit('close-modal')
}
</script>

场景3:子组件列表删除,父组件更新列表

<!-- 子组件(ListChild.vue) -->
<template>
  <div class="list-child">
    <div v-for="item in list" :key="item.id">
      {{ item.name }}
      <button @click="handleDelete(item.id)">删除</button>
    </div>
  </div>
</template>

<script setup>
const props = defineProps(['list'])
const emit = defineEmits(['delete-item'])

const handleDelete = (id) => {
  // 传递要删除的id给父组件,由父组件更新列表
  emit('delete-item', id)
}
</script>

五、常见坑点避坑指南(新手必看)

很多新手在写子传父时,会遇到「父组件监听不到事件」「数据传递失败」等问题,以下是最常见的 4 个坑点,帮你快速避坑。

坑点1:事件名大小写不一致

子组件 emit('inputChange'),父组件 @inputchange="handle"(小写),会导致父组件监听不到事件。

解决方案:统一采用 kebab-case 命名,子组件 emit('input-change'),父组件 @input-change="handle"

坑点2:忘记声明事件(defineEmits)

子组件直接调用 emit('event'),未用 defineEmits 声明事件,虽然开发环境可能不报错,但生产环境可能出现异常,且 IDE 无提示。

解决方案:无论事件是否需要校验,都用 defineEmits 声明(数组形式即可)。

坑点3:传递复杂数据(对象/数组)时,父组件修改后影响子组件

子组件传递对象/数组给父组件,父组件直接修改该数据,会影响子组件(因为引用类型传递的是地址)。

解决方案:父组件接收数据后,用 JSON.parse(JSON.stringify(data)) 深拷贝,或用 reactive + toRaw 处理,避免直接修改原始数据。

坑点4:v-model 双向绑定时报错,提示「modelValue 未定义」

原因:子组件未接收 modelValue props,或未声明 update:modelValue 事件。

解决方案:确保子组件 defineProps(['modelValue'])defineEmits(['update:modelValue']) 都声明。

六、总结:子传父核心要点回顾

Vue3 子传父的核心就是「事件触发 + 事件监听」,记住以下 3 个核心要点,就能应对所有场景:

  1. 基础写法:defineEmits 声明事件 → emit 触发事件 → 父组件 @事件名 监听;
  2. 进阶优化:事件校验提升可靠性,v-model 简化双向绑定,遵循 kebab-case 命名规范;
  3. 避坑关键:事件名大小写一致、必声明事件、复杂数据深拷贝、v-model 对应 props 和 emit 命名正确。

子传父是 Vue3 组件通信中最基础的方式,掌握它之后,再学习父传子(props)、跨层级通信(provide/inject)、全局通信(Pinia)会更轻松。