Code前端首页关于Code前端联系我们

Vue2里的$emit到底怎么用?从基础到实战全解析

terry 3小时前 阅读数 8 #Vue
文章标签 Vue2;$emit

做Vue2项目时,组件之间的通信是不是让你头大?尤其是子组件想给父组件传数据、发通知的时候,$emit 就是关键工具!但刚学的时候,是不是经常碰到“事件名写错监听不到”“参数传了但父组件拿不到”这些问题?今天咱们从头梳理 $emit 的用法、避坑点,还有实战场景怎么玩,帮你把组件通信这块儿吃透~

$emit 是干啥的?解决什么场景的问题?

先想清楚组件通信的逻辑:Vue 里组件是分层嵌套的,父组件管子组件,但子组件想主动“告诉”父组件点事儿(比如点击按钮、数据变化),这时候就得用 子传父 的通信方式,$emit 就是专门干这个的!

举个生活例子:你点外卖(子组件是“下单按钮”),点击后得通知商家(父组件是“店铺页面”)做餐,这时候“点击按钮”就是触发 $emit 事件,“商家收到通知做菜”就是父组件监听事件后的操作。

具体技术场景:

  • 子组件按钮点击,父组件更新数据(比如列表新增项)
  • 子组件表单输入变化,父组件实时校验
  • 子组件弹窗关闭,父组件隐藏弹窗

没有 $emit 的话,子组件根本没法主动触发父组件的逻辑,只能等父组件把方法通过 props 传进来(但 props 是父传子,反过来用容易乱,而且不符合设计逻辑),$emit 是官方推荐的子传父通信“正规军”~

基础用法:$emit 咋写?分几步?

核心逻辑就两步:子组件触发事件父组件监听事件

子组件里触发 $emit

语法:this.$emit('事件名', 要传的数据)

  • “事件名”:自定义的名字,'add-item' 'input-change',注意 Vue2 里模板中事件名要用短横线命名(kebab-case),因为 HTML 属性不区分大小写,比如你写 @myEvent 其实会被解析成 @myevent,所以子组件和父组件的事件名要统一用短横线(如 my-event),避免歧义。
  • “要传的数据”:可以是字符串、数字、对象、数组,甚至多个数据(后面讲多参数传递)。

举个简单例子(子组件是 ChildButton.vue):

<template>
  <button @click="handleClick">点我通知父组件</button>
</template>
<script>
export default {
  methods: {
    handleClick() {
      // 触发自定义事件,并传递数据(这里传个字符串)
      this.$emit('btn-click', '我是子组件传来的消息~');
    }
  }
}
</script>

父组件里监听事件

语法:在子组件标签上用 @事件名="父组件的方法"

<template>
  <div>
    <!-- 监听子组件的btn-click事件,触发parentMethod -->
    <ChildButton @btn-click="parentMethod" />
    <p>{{ message }}</p>
  </div>
</template>
<script>
import ChildButton from './ChildButton.vue'
export default {
  components: { ChildButton },
  data() { return { message: '' } },
  methods: {
    parentMethod(data) { // data 就是子组件传来的内容
      this.message = data; // 把消息显示到页面上
    }
  }
}
</script>

这样点击子组件按钮,父组件的 parentMethod 就会执行,把数据拿到并更新页面~

传多个参数咋整?父组件咋接收?

子组件想一次性传多个数据,比如同时传“商品ID”和“数量”,咋做?

子组件传多参数

直接在 $emit 里跟多个值:

this.$emit('add-to-cart', productId, quantity); // 传两个参数

父组件接收多参数

有两种方式:

方式1:父组件方法直接接收参数

父组件的方法参数和子组件传的顺序一一对应:

<Child @add-to-cart="handleAdd" />
// ...
methods: {
  handleAdd(productId, quantity) {
    console.log(productId, quantity); // 子组件传的两个参数
  }
}

方式2:用 $event 手动指定参数

如果父组件想在绑定事件时,自己加额外参数,就用 $event 代表子组件传来的参数(类似原生事件的 event 对象),比如子组件传了 productId,父组件想再加个固定值 10

<Child @add-to-cart="handleAdd($event, 10)" />
// ...
methods: {
  handleAdd(productId, myNum) {
    console.log(productId, myNum); // 子组件的productId + 父组件的10
  }
}

注意:$event 只能对应 子组件传来的第一个参数,如果子组件传了多个,(id, num),父组件用 $event 只能拿到 id,第二个参数得用参数名接收,所以传多参数时,更推荐方式1,逻辑更清晰~

事件名总冲突?咋规范命名?

项目大了,多个组件都用 'click' 'change' 当事件名,父组件监听时根本分不清是哪个子组件触发的!这时候得 给事件名加“作用域”,避免重复。

命名技巧

  • 组件前缀法:用组件名当前缀,比如组件叫 UserCard,事件名可以是 user-card-click user-card-update
  • 功能模块法:按业务模块分,比如购物车模块的事件叫 cart-add cart-delete,订单模块叫 order-submit order-cancel
  • 语义明确法:事件名要能看出“发生了什么”,form-validate-pass(表单验证通过)比 form-ok 更清晰。

举个规范的例子(子组件 ProductItem.vue):

// 触发“商品项点击”事件,传递商品ID
this.$emit('product-item-click', productId);

父组件监听:

<ProductItem @product-item-click="handleProductClick" />

这样别人看代码时,一眼就知道是 ProductItem 组件触发的 click 事件,不会和其他组件的 click 搞混~

实战:用 $emit 实现复杂交互(拿购物车举例)

光讲理论太虚,咱们搞个真实场景:子组件是商品卡片,点击“加入购物车”后,父组件更新购物车列表并提示

步骤1:子组件(ProductCard.vue)触发事件

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>价格:{{ product.price }}</p>
    <button @click="addToCart">加入购物车</button>
  </div>
</template>
<script>
export default {
  props: {
    product: { type: Object, required: true }
  },
  methods: {
    addToCart() {
      // 触发addToCart事件,传递商品信息
      this.$emit('add-to-cart', this.product);
    }
  }
}
</script>

步骤2:父组件(CartPage.vue)监听并处理

<template>
  <div>
    <h2>购物车页面</h2>
    <div class="product-list">
      <!-- 循环渲染商品卡片,每个卡片监听add-to-cart事件 -->
      <ProductCard 
        v-for="(item, index) in productList" 
        :key="index" 
        :product="item" 
        @add-to-cart="handleAddCart" 
      />
    </div>
    <div class="cart-summary">
      <h3>已选商品:</h3>
      <ul>
        <li v-for="(item, index) in cartList" :key="index">
          {{ item.name }} - ¥{{ item.price }}
        </li>
      </ul>
    </div>
  </div>
</template>
<script>
import ProductCard from './ProductCard.vue'
export default {
  components: { ProductCard },
  data() {
    return {
      productList: [ // 假设的商品列表
        { name: '手机', price: 3999 },
        { name: '耳机', price: 499 }
      ],
      cartList: [] // 购物车列表
    }
  },
  methods: {
    handleAddCart(product) {
      // 把商品加入购物车
      this.cartList.push(product);
      // 提示用户(可以用UI库的Toast,这里模拟alert)
      alert(`${product.name} 已加入购物车~`);
    }
  }
}
</script>

延伸:双向交互(父组件处理后通知子组件)

如果父组件加完购物车后,要告诉子组件“已加入,按钮变禁用”,咋做?这时候需要 父组件通过 props 传状态,子组件根据状态更新

  • 子组件新增 isAdded props,控制按钮是否禁用:

    <template>
      <button 
        @click="addToCart" 
        :disabled="isAdded"
      >
        {{ isAdded ? '已加入' : '加入购物车' }}
      </button>
    </template>
    <script>
    export default {
      props: {
        product: Object,
        isAdded: { type: Boolean, default: false }
      },
      // ...
    }
    </script>
  • 父组件维护每个商品的 isAdded 状态,监听 add-to-cart 后更新,并把状态传给子组件:

    <template>
      <ProductCard 
        ...
        :is-added="item.isAdded" 
      />
    </template>
    <script>
    export default {
      data() {
        return {
          productList: [
            { name: '手机', price: 3999, isAdded: false },
            { name: '耳机', price: 499, isAdded: false }
          ],
          // ...
        }
      },
      methods: {
        handleAddCart(product, index) { // 注意这里要传index
          this.productList[index].isAdded = true;
          this.cartList.push(product);
          // ...
        }
      }
    }
    </script>

这样就实现了 子组件触发事件 → 父组件处理并更新状态 → 父组件通过 props 把状态传回子组件 的完整交互,$emit 在其中负责“子传父”的关键一步~

这些坑你肯定踩过!避坑指南

坑1:事件名大小写/格式写错,监听不到

Vue2 中,父组件模板里的事件名是 区分短横线和驼峰 的!比如子组件用 this.$emit('myEvent'),父组件写 @my-event 才能监听到(因为 HTML 属性会把驼峰转成小写,@myEvent 实际变成 @myevent,和子组件的 myEvent 不匹配)。

解决:统一用短横线命名事件,子组件和父组件都写 'my-event',避免大小写歧义。

坑2:父组件忘写 @事件名,白触发

子组件里 this.$emit('xxx') 写了,但父组件引用子组件时没加 @xxx="方法",那事件永远触发不了。

解决:写代码时,先确定子组件要触发什么事件,父组件立刻加上监听,养成配对写的习惯。

坑3:传对象/数组后,父组件修改影响子组件

子组件传了个对象 this.$emit('send', { name: '张三' }),父组件拿到后修改 data.name = '李四',结果子组件里的原对象也变了(因为引用类型传的是地址)。

解决:如果不想子组件数据被影响,传参时深拷贝,比如用 JSON.parse(JSON.stringify(obj)) 或者 lodash 的 cloneDeep

坑4:循环里多次触发事件,逻辑重复执行

比如子组件在 v-for 里,每次循环都触发 $emit,导致父组件方法执行多次。

解决:检查触发条件,比如加 if 判断(只有满足条件才触发),或者用防抖(debounce)控制触发频率。

坑5:混淆 $emit 和 props 的方向

props父传子$emit子传父,如果搞反了(比如父组件用 $emit 给子组件传数据),逻辑全乱套。

解决:记住方向:父→子用 props,子→父用 $emit,实在记不住就想“父是老大,主动给子东西(props);子是小弟,有事找老大得喊($emit)”~

和其他通信方式比,$emit 有啥优势?

Vue2 里组件通信方式不少,props、事件总线(event bus)、Vuex,$emit 适合 组件层级不深、仅需子传父 的场景,优势很明显:

  • 对比 propsprops 是父传子,$emit 是子传父,方向互补,官方推荐的“父子通信组合拳”。
  • 对比 事件总线:事件总线(比如用 new Vue() 当全局事件中心)容易导致“全局污染”,多个组件乱发事件不好维护;而 $emit组件级通信,事件只在父组件和子组件之间传递,范围可控,不容易冲突。
  • 对比 Vuex:Vuex 是全局状态管理,适合多个组件跨层级共享数据;但如果只是简单的子传父(比如一个按钮点击),用 $emit 更轻量,不用引入复杂的全局状态管理。

现在再回头看 $emit,是不是觉得逻辑清晰多了?从基础语法到传参技巧,再到实战和避坑,核心就是“子组件触发事件传数据,父组件监听事件做处理”,记住命名规范、参数传递、场景匹配这几点,组件通信再也不懵~下次写项目时,碰到子传父的需求,直接用 $emit 安排上就完事儿!要是还有疑问,评论区随时喊我,咱们再唠~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门