Vue Emits详解:子传父的核心通信方式

0 阅读14分钟

在Vue组件化开发中,组件间的通信是双向的:props负责“父传子”,将父组件的数据传递给子组件供其使用;而Emits(自定义事件)则负责“子传父”,让子组件能够向父组件传递数据、触发父组件的方法,实现父子组件的双向联动。

上一篇我们详解了props的用法,解决了父组件向子组件传递数据的问题,但在实际开发中,子组件往往需要反馈信息给父组件——比如点击子组件的按钮修改父组件的状态、子组件表单输入后同步数据到父组件、子组件完成操作后通知父组件刷新等。此时,Emits就成为了不可或缺的核心工具。

本文将从Emits的本质出发,一步步拆解其声明方式、触发技巧、监听方法,结合博客开发的实战场景,用全新自定义代码示例,帮你彻底掌握Emits的核心用法,理解props与Emits的配合逻辑,实现组件间高效、规范的双向通信。

一、Emits 是什么?核心作用是什么?

Emits 即Vue中的自定义事件,是子组件向父组件传递信息、触发父组件逻辑的核心方式。它就像子组件给父组件的“回调通知”:子组件通过触发自定义事件,将数据或操作信号传递给父组件,父组件通过监听该事件,执行对应的处理逻辑。

Emits 的核心作用有三个,完美弥补props单向数据流的局限性:

  • 实现子传父数据传递:子组件可以将自身的状态、用户操作结果等数据,通过Emits传递给父组件;
  • 触发父组件方法:子组件的操作(如点击按钮、表单提交)可以通过Emits通知父组件,执行父组件中的方法;
  • 保持数据流向清晰:不同于直接修改父组件数据,Emits通过“子组件触发事件、父组件处理事件”的方式,保证数据修改的可控性,符合Vue单向数据流的设计理念。

举个贴合前文博客场景的例子:我们的博客列表中,每篇博客卡片(子组件)有一个“收藏”按钮,点击按钮后,需要将该博客的ID传递给父组件,由父组件更新收藏列表。此时,就可以通过Emits实现:子组件点击按钮时触发自定义事件,传递博客ID;父组件监听该事件,接收ID并更新数据。

二、Emits 的基础用法:声明、触发与监听

使用Emits只需三步:子组件声明要触发的自定义事件、子组件触发事件(可携带参数)、父组件监听事件并处理。与props类似,Emits的声明方式也分为

1. 子组件:声明并触发自定义事件

在< script setup>中,我们使用Vue内置的编译宏defineEmits来声明子组件要触发的自定义事件,无需显式导入,直接使用即可。声明完成后,通过defineEmits返回的emit函数触发事件,可携带任意数量的参数(用于向父组件传递数据)。

步骤1:声明自定义事件

defineEmits接收一个数组,数组中的每一项是自定义事件的名称(建议使用kebab-case命名规范,与HTML原生事件风格保持一致,提升可读性)。

示例:子组件(BlogItem.vue),声明“收藏博客”和“取消收藏”两个自定义事件:

<script setup>
// 声明要触发的自定义事件:collect-blog(收藏)、cancel-collect(取消收藏)
const emit = defineEmits(['collect-blog', 'cancel-collect'])

// 接收父组件传递的props
const props = defineProps({
  blog: {
    type: Object,
    required: true,
    default: () => ({})
  }
})
</script>

注意:声明事件后,Vue会对事件进行校验,避免子组件触发未声明的事件,同时也能让代码更具可读性,方便其他开发者了解组件可触发的事件。

步骤2:触发自定义事件(可携带参数)

通过defineEmits返回的emit函数触发事件,语法为:emit(事件名称, 参数1, 参数2, ...)。其中,第一个参数是事件名称(必须与声明的一致),后续参数是要传递给父组件的数据,可根据需求传递任意类型、任意数量的参数。

示例:完善BlogItem.vue,添加收藏/取消收藏按钮,点击按钮触发对应事件,传递博客ID:

<script setup>
const emit = defineEmits(['collect-blog', 'cancel-collect'])
const props = defineProps({
  blog: {
    type: Object,
    required: true,
    default: () => ({})
  },
  isCollected: { // 父组件传递的收藏状态,用于切换按钮文本
    type: Boolean,
    default: false
  }
})

// 点击收藏按钮,触发collect-blog事件,传递博客ID
const handleCollect = () => {
  // 传递博客ID(核心数据)和博客标题(辅助数据)
  emit('collect-blog', props.blog.id, props.blog.title)
}

// 点击取消收藏按钮,触发cancel-collect事件,传递博客ID
const handleCancelCollect = () => {
  emit('cancel-collect', props.blog.id)
}
</script>

<template>
  <div class="blog-item">
    <h3 class="blog-title">{{ blog.title }}</h3>
    <p class="blog-summary">{{ blog.summary || '暂无摘要' }}</p>
    <div class="blog-actions">
      <button @click="handleCollect" v-if="!isCollected">收藏博客</button>
      <button @click="handleCancelCollect" v-if="isCollected">取消收藏</button>
    </div>
  </div>
</template>

<style scoped>
.blog-item {
  padding: 16px;
  border-bottom: 1px solid #eee;
  margin-bottom: 12px;
}
.blog-title {
  margin: 0 0 8px 0;
  color: #333;
  font-size: 18px;
}
.blog-summary {
  margin: 0 0 12px 0;
  color: #666;
  font-size: 14px;
  line-height: 1.5;
}
.blog-actions button {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background-color: #42b983;
  color: white;
}
.blog-actions button:nth-child(2) {
  background-color: #f56c6c;
  margin-left: 8px;
}
</style>

这里,我们通过emit函数向父组件传递了博客ID和标题,父组件可以接收这些参数,执行收藏或取消收藏的逻辑——这就是子传父数据传递的核心逻辑。

2. 父组件:监听自定义事件并处理

子组件触发自定义事件后,父组件需要通过v-on语法(简写为@)监听该事件,然后定义处理函数,接收子组件传递的参数并执行对应逻辑。

监听事件的语法有两种:简洁写法(直接执行简单逻辑)和完整写法(调用方法,处理复杂逻辑),可根据需求选择。

方式1:简洁写法(适用于简单逻辑)

如果处理逻辑简单(如修改单个响应式数据),可以直接在监听事件时编写逻辑,无需单独定义方法。

示例:父组件(BlogList.vue),监听子组件的“放大字体”事件(贴合参考场景,简化逻辑):

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

// 响应式数据:博客列表、字体大小、收藏列表
const blogs = ref([
  { id: 1, title: 'Vue props 实战指南', summary: '详解props父传子用法' },
  { id: 2, title: 'Vue Emits 核心技巧', summary: '掌握子传父通信方式' },
  { id: 3, title: 'Vue 组件通信全解析', summary: 'props与Emits配合使用' }
])
const fontSize = ref(1) // 字体大小,默认1em
</script>

<template>
  <div class="blog-list" :style="{ fontSize: fontSize + 'em' }">
    <h2>我的博客列表</h2>
    <BlogItem 
      v-for="blog in blogs"
      :key="blog.id"
      :blog="blog"
      <!-- 监听子组件的放大字体事件直接修改fontSize -->
      @enlarge-text="fontSize += 0.1"
    />
  </div>
</template>

方式2:完整写法(推荐,适用于复杂逻辑)

实际开发中,处理逻辑往往更复杂(如更新收藏列表、请求接口、校验数据等),此时建议单独定义处理函数,让代码更具可读性和可维护性。

示例:完善BlogList.vue,监听子组件的收藏/取消收藏事件,处理收藏列表更新:

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

// 响应式数据
const blogs = ref([
  { id: 1, title: 'Vue props 实战指南', summary: '详解props父传子用法' },
  { id: 2, title: 'Vue Emits 核心技巧', summary: '掌握子传父通信方式' },
  { id: 3, title: 'Vue 组件通信全解析', summary: 'props与Emits配合使用' }
])
const collectedBlogs = ref([]) // 收藏列表

// 处理子组件的收藏事件:接收子组件传递的博客ID和标题
const handleCollectBlog = (blogId, blogTitle) => {
  // 避免重复收藏
  if (!collectedBlogs.value.some(item => item.id === blogId)) {
    collectedBlogs.value.push({ id: blogId, title: blogTitle })
    alert(`已收藏博客:${blogTitle}`)
  } else {
    alert('该博客已收藏')
  }
}

// 处理子组件的取消收藏事件:接收子组件传递的博客ID
const handleCancelCollect = (blogId) => {
  collectedBlogs.value = collectedBlogs.value.filter(item => item.id !== blogId)
  const blog = blogs.value.find(item => item.id === blogId)
  alert(`已取消收藏博客:${blog.title}`)
}
</script>

<template>
  <div class="blog-list">
    <h2>我的博客列表</h2>
    <div class="collected-info">
      <h3>我的收藏({{ collectedBlogs.length }})</h3>
      <p v-if="collectedBlogs.length === 0">暂无收藏,快去收藏喜欢的博客吧~</p>
      <ul v-else>
        <li v-for="item in collectedBlogs" :key="item.id">{{ item.title }}</li>
      </ul>
    </div>
    
    <BlogItem 
      v-for="blog in blogs"
      :key="blog.id"
      :blog="blog"
      <!-- 监听子组件的自定义事件绑定处理函数 -->
      @collect-blog="handleCollectBlog"
      @cancel-collect="handleCancelCollect"
      <!-- 向子组件传递收藏状态,用于切换按钮 -->
      :is-collected="collectedBlogs.some(item => item.id === blog.id)"
    />
  </div>
</template>

<style scoped>
.blog-list {
  padding: 20px;
}
.collected-info {
  margin-bottom: 20px;
  padding: 16px;
  border: 1px solid #eee;
  border-radius: 8px;
}
.collected-info h3 {
  margin: 0 0 8px 0;
  color: #333;
}
.collected-info p {
  margin: 0;
  color: #666;
}
.collected-info ul {
  margin: 8px 0 0 0;
  padding-left: 20px;
}
</style>

这里有两个关键注意点:

  • 父组件监听事件时,事件名称必须与子组件声明、触发的名称完全一致(区分大小写,建议统一使用kebab-case);
  • 子组件emit传递的参数,会按顺序作为父组件处理函数的参数,父组件可根据需求接收全部或部分参数。

三、非 < script setup> 语法:Emits 的声明与使用

如果不使用< script setup>语法(传统选项式API),Emits的声明方式需要通过emits选项来实现,且emit函数需要从setup()函数的第二个参数(上下文对象ctx)中获取。

示例:非< script setup>方式的子组件(BlogItem.vue):

<script>
export default {
  // 选项式API声明要触发的自定义事件
  emits: ['collect-blog', 'cancel-collect'],
  // 接收父组件传递的props
  props: {
    blog: {
      type: Object,
      required: true,
      default: () => ({})
    },
    isCollected: {
      type: Boolean,
      default: false
    }
  },
  // setup函数:第二个参数ctx包含emit方法
  setup(props, ctx) {
    // 点击收藏,通过ctx.emit触发事件
    const handleCollect = () => {
      ctx.emit('collect-blog', props.blog.id, props.blog.title)
    }

    // 点击取消收藏
    const handleCancelCollect = () => {
      ctx.emit('cancel-collect', props.blog.id)
    }

    return { handleCollect, handleCancelCollect }
  },
  // 模板与<script setup>一致
  template: `
    <div class="blog-item">
      <h3 class="blog-title">{{ blog.title }}</h3>
      <p class="blog-summary">{{ blog.summary || '暂无摘要' }}</p>
      <div class="blog-actions">
        <button @click="handleCollect" v-if="!isCollected">收藏博客</button>
        <button @click="handleCancelCollect" v-if="isCollected">取消收藏</button>
      </div>
    </div>
  `
}
</script>

这种方式的核心逻辑与

四、Emits 的进阶用法:事件校验

除了基础的声明和触发,Emits还支持事件校验——可以对子组件传递的参数进行校验,确保传递的数据符合父组件的预期,提升代码健壮性。当校验失败时,Vue会在控制台抛出警告,帮助我们快速定位问题。

事件校验的实现方式:defineEmits接收一个对象,对象的key是事件名称,value是校验函数(接收子组件传递的参数,返回布尔值,true表示校验通过,false表示校验失败)。

示例:子组件(BlogItem.vue),对“collect-blog”事件的参数进行校验,确保博客ID是数字类型:

<script setup>
// 事件校验:对象形式声明事件,value为校验函数
const emit = defineEmits({
  // 校验collect-blog事件的参数:第一个参数(blogId)必须是数字
  'collect-blog': (blogId, blogTitle) => {
    if (typeof blogId !== 'number') {
      console.warn('博客ID必须是数字类型!')
      return false // 校验失败
    }
    return true // 校验通过
  },
  // 取消收藏事件无需校验,直接声明
  'cancel-collect': null
})

const props = defineProps({
  blog: { type: Object, required: true },
  isCollected: { type: Boolean, default: false }
})

const handleCollect = () => {
  // 模拟传递错误类型的ID(字符串),触发校验警告
  emit('collect-blog', String(props.blog.id), props.blog.title)
}

const handleCancelCollect = () => {
  emit('cancel-collect', props.blog.id)
}
</script>

事件校验的核心价值的是:在开发阶段就发现子组件传递的参数错误,避免因数据类型不符导致父组件处理逻辑异常,尤其适合多人协作开发的场景。

五、Emits 与 Props 配合:实现父子组件双向通信

Props负责父传子,Emits负责子传父,两者结合就能实现父子组件的双向通信,这是Vue组件通信中最常用、最基础的模式。我们通过一个完整的实战场景,演示两者的配合使用。

实战场景:博客详情页,子组件(BlogContent.vue)展示博客内容,包含“修改标题”按钮,点击按钮弹出输入框,输入新标题后,通过Emits传递给父组件,父组件更新原始数据,再通过Props同步给子组件,实现标题的双向更新。

1. 子组件(BlogContent.vue):接收Props、触发Emits

<script setup>
// 声明要触发的事件:update-title(修改标题)
const emit = defineEmits(['update-title'])

// 接收父组件传递的props:博客详情
const props = defineProps({
  blog: {
    type: Object,
    required: true,
    default: () => ({
      title: '',
      content: '',
      publishTime: new Date()
    })
  }
})

// 点击修改标题,触发事件传递新标题
const handleUpdateTitle = () => {
  const newTitle = prompt('请输入新的博客标题', props.blog.title)
  if (newTitle && newTitle.trim() !== '') {
    // 传递新标题给父组件
    emit('update-title', newTitle.trim())
  }
}
</script>

<template>
  <div class="blog-detail">
    <h1 class="detail-title">{{ blog.title }}</h1>
    <p class="publish-time">发布时间:{{ blog.publishTime.toLocaleDateString() }}</p>
    <div class="detail-content" v-html="blog.content"></div>
    <button class="update-btn" @click="handleUpdateTitle">修改标题</button>
  </div>
</template>

<style scoped>
.blog-detail {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}
.detail-title {
  color: #333;
  font-size: 24px;
  margin: 0 0 16px 0;
  text-align: center;
}
.publish-time {
  color: #666;
  font-size: 14px;
  margin: 0 0 20px 0;
  text-align: center;
}
.detail-content {
  line-height: 1.8;
  color: #444;
  font-size: 16px;
  margin-bottom: 20px;
}
.update-btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background-color: #4299e1;
  color: white;
}
</style>

2. 父组件(BlogDetail.vue):传递Props、监听Emits

<script setup>
import { ref, onMounted } from 'vue'
import BlogContent from './BlogContent.vue'

// 响应式博客详情数据
const blogDetail = ref({})

// 模拟接口请求获取博客详情
onMounted(() => {
  setTimeout(() => {
    blogDetail.value = {
      title: 'Vue Emits 实战指南',
      content: `<p>Emits是Vue中子传父的核心通信方式,与Props配合使用,可实现父子组件双向通信。</p>
      <p>本文通过实战场景,详细讲解了Emits的声明、触发、监听及事件校验用法。</p>`,
      publishTime: new Date('2026-04-16')
    }
  }, 1000)
})

// 处理子组件的修改标题事件,更新父组件数据
const handleUpdateTitle = (newTitle) => {
  blogDetail.value.title = newTitle
}
</script>

<template>
  <div class="blog-detail-page">
    <div v-if="!blogDetail.title" class="loading">加载中...</div>
    
    <BlogContent
      v-else
      :blog="blogDetail"  <!-- 父传子传递博客详情 -->
      @update-title="handleUpdateTitle"  <!-- 监听子组件事件,处理标题更新 -->
    />
  </div>
</template>

<style scoped>
.loading {
  text-align: center;
  padding: 50px;
  font-size: 18px;
  color: #666;
}
</style>

这个场景完美体现了Props与Emits的配合逻辑:

  1. 父组件通过Props将博客详情数据传递给子组件,子组件渲染内容;
  2. 子组件点击“修改标题”按钮,输入新标题后,通过Emits将新标题传递给父组件;
  3. 父组件监听事件,接收新标题并更新自身的响应式数据;
  4. 父组件数据更新后,通过Props自动同步给子组件,子组件重新渲染新标题。

六、Emits 的核心注意事项:避开这些坑

使用Emits时,有几个核心注意事项必须牢记,避免出现事件不触发、数据传递异常等问题,尤其是初学者容易踩坑。

1. 事件名称要规范,避免冲突

建议使用kebab-case(短横线连字符)命名事件(如collect-blog、update-title),与HTML原生事件风格保持一致,避免使用camelCase(如collectBlog)——因为HTML标签属性不区分大小写,可能导致事件监听失败。同时,避免与Vue内置事件(如click、input)重名,否则会覆盖原生事件。

2. 必须声明事件,避免隐式触发

子组件触发的自定义事件,必须通过defineEmits(或emits选项)声明,否则Vue会将其作为原生事件隐式应用于子组件的根元素,可能导致意外行为。同时,声明事件也能提升代码可读性,方便其他开发者了解组件的交互逻辑。

3. 避免直接在子组件中修改父组件数据

即使通过Emits传递了数据,也必须由父组件自己修改自身的数据,子组件不能直接修改父组件传递的Props或父组件的响应式数据——这是Vue单向数据流的核心原则,避免数据流向混乱,便于后续维护和调试。

4. 事件参数不宜过多

子组件通过emit传递参数时,建议参数数量不超过3个,若需要传递多个数据,可将其封装成一个对象,避免参数过多导致代码混乱、维护困难。

示例(推荐写法):

// 子组件:传递对象形式的参数
emit('collect-blog', { id: props.blog.id, title: props.blog.title })

// 父组件:接收对象参数
const handleCollectBlog = (blogInfo) => {
  console.log(blogInfo.id, blogInfo.title)
}

七、总结:Emits 是子传父的核心,与Props相辅相成

Emits作为Vue中子传父通信的核心方式,与Props共同构成了父子组件通信的基础,两者相辅相成:Props负责“输入”(父传子),Emits负责“输出”(子传父),结合使用就能实现组件间的双向联动,打造结构清晰、可维护、可复用的Vue组件。

最后,总结一下Emits的核心要点:

  • Emits是子组件向父组件传递数据、触发逻辑的自定义事件,遵循单向数据流原则;
  • 子组件通过defineEmits声明事件,通过emit函数触发事件(可携带参数);
  • 父组件通过@监听事件,通过处理函数接收参数并执行逻辑;
  • 支持事件校验,可确保传递的数据符合预期,提升代码健壮性;
  • 与Props配合,实现父子组件双向通信,是Vue组件开发中最常用的通信模式。

掌握Emits的用法,能让你在Vue组件开发中更灵活地处理组件间的交互,解决子传父的通信需求。后续我们还会讲解兄弟组件通信、跨层级组件通信等高级技巧,让你彻底掌握Vue组件通信的全场景用法,打造更优秀的Vue应用。