Appearance
✨ 自定义事件 👌
要点速览
- 子组件通过自定义事件把数据“向上”传给父组件,配合 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-rating、form-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 共同构成单向数据流。接下来建议继续学习:
- 插槽(结构与内容扩展)
- 组合使用:Props + 事件 + 插槽构建可复用的复杂组件
学习建议
- 先掌握事件声明、触发、校验与
v-model约定。 - 然后结合 Props、插槽进行组件的受控设计与扩展。
