Skip to content

✨ 自定义事件 👌

要点速览

  • 子组件通过自定义事件把数据“向上”传给父组件,配合 Props 形成单向数据流。
  • <script setup> 中用 defineEmits() 声明事件;在模板中也可直接使用 $emit
  • 事件名在模板中推荐使用短横线:@update-rating@submit;大小写不敏感但建议统一。
  • 事件可校验 payload(defineEmits({ event: validator })),便于在开发阶段发现问题。
  • v-model 的约定:接收 modelValue,触发 update:modelValue;支持多 v-model
  • 父级监听可用 .once 修饰符;其他修饰符多用于 DOM 事件,不适用于组件事件。

快速上手

以评分组件为例,子组件触发 update-rating 将最新分值传递给父组件:

vue
<template>
  <div class="rating-container">
    <span v-for="star in 5" :key="star" class="star" @click="setStar(star)">
      {{ rating >= star ? "★" : "☆" }}
    </span>
  </div>
</template>

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

const rating = ref(0);
const emit = defineEmits(["update-rating"]);

function setStar(newStar) {
  rating.value = newStar;
  emit("update-rating", rating.value);
}
</script>

<style scoped>
.rating-container {
  display: flex;
  font-size: 24px;
  cursor: pointer;
}
.star {
  margin-right: 5px;
  color: gold;
}
.star:hover {
  color: orange;
}
</style>

父组件监听并接收子组件传回的分值:

vue
<template>
  <div class="app-container">
    <h1>请对本次服务评分:</h1>
    <Rating @update-rating="handleRating" />
    <p v-if="rating > 0">你当前的评价为 {{ rating }} 颗星</p>
  </div>
</template>

<script setup>
import { ref } from "vue";
import Rating from "./components/Rating.vue";

const rating = ref(0);
function handleRating(newRating) {
  rating.value = newRating;
}
</script>

<style scoped>
.app-container {
  max-width: 600px;
  margin: auto;
  text-align: center;
  font-family: Arial, sans-serif;
}
p {
  font-size: 18px;
  color: #333;
}
</style>

事件命名

  • 推荐短横线命名,避免与变量名产生歧义:update-ratingform-submit
  • 父模板监听时保持一致的命名;不建议在事件名中使用空格或特殊字符。

模板中直接触发事件

在模板中无需显式声明即可使用 $emit

vue
<span
  v-for="star in 5"
  :key="star"
  class="star"
  @click="$emit('update-rating', star)"
>
  {{ rating >= star ? '★' : '☆' }}
</span>

何时选择 $emit

  • 简单场景、仅在模板中触发时使用 $emit 更直接。
  • 若在脚本中多处触发或需要校验,使用 defineEmits() 更规范。

事件校验(开发期)

类似 Props 校验,事件也可校验 payload:

vue
<script setup>
const emit = defineEmits({
  // 无校验
  click: null,

  // 对提交事件进行校验
  submit: ({ email, password }) => {
    const ok = Boolean(email && password);
    if (!ok) console.warn("Invalid submit event payload!");
    return ok;
  },
});

function submitForm(email: string, password: string) {
  emit("submit", { email, password });
}
</script>

评分组件的校验示例:

vue
<script setup>
const emit = defineEmits({
  "update-rating": (value: number) => {
    if (value < 1 || value > 5) {
      console.warn("评分必须在 1~5 范围内");
      return false;
    }
    return true;
  },
});
</script>

常见误区

  • 事件校验失败不会阻止父组件接收值(开发期仅告警),仍需在父层做必要的防御性处理。
  • 事件名与父层监听名不一致(例如大小写差异)导致回调未触发。

与 Props 的关系(单向数据流)

子组件不应直接修改从 Props 接收到的数据,需通过事件请求父组件更新:

js
// 子组件
const props = defineProps({ count: Number });
const emit = defineEmits(["update:count"]);

function increment() {
  emit("update:count", props.count + 1);
}

父组件用受控方式维护数据:

vue
<Counter :count="count" @update:count="count = $event" />

额外细节

组织约定

  • defineEmits 与触发逻辑放在一起,便于维护。
  • 对公共事件与 payload 进行类型文档化,减少上下游不一致。

TypeScript(可选)

defineEmits 提供事件签名,更直观地约束触发与监听:

ts
type Events = {
  (e: "update-rating", value: number): void;
  (e: "submit", payload: { email: string; password: string }): boolean;
};

const emit = defineEmits<Events>();

emit("update-rating", 5);
emit("submit", { email: "a@b.com", password: "***" });

小结与后续

自定义事件是子 → 父通信的关键,与 Props 共同构成单向数据流。接下来建议继续学习:

  1. 插槽(结构与内容扩展)
  2. 组合使用:Props + 事件 + 插槽构建可复用的复杂组件

学习建议

  • 先掌握事件声明、触发、校验与 v-model 约定。
  • 然后结合 Props、插槽进行组件的受控设计与扩展。