Vue自定义事件:父子通信的工程化契约与最佳实践
1. 这不是“学个语法”那么简单:Vue自定义事件到底在解决什么真实问题?
刚接触 Vue 的人常把 $emit 当成一个“触发通知”的小工具,点个按钮就 this.$emit('click') ,再在父组件用 v-on:click 接住——看起来和原生 DOM 事件没两样。但真正带过三个以上中型 Vue 项目、重构过遗留代码的开发者会立刻意识到: 自定义事件是 Vue 组件通信的底层契约,它直接决定了组件的可复用性、可测试性,甚至整个应用的状态流向是否可控。 我自己在做一套内部表单引擎时踩过最深的坑,就是早期图省事,让子组件直接调用父组件的 methods,结果改一个字段校验逻辑,要翻遍七八个页面的 <form-item> 实例,最后花两天时间全量替换成 $emit + v-model 双向绑定,才让后续迭代效率翻倍。
为什么说它关键?因为 Vue 的设计哲学是“单向数据流”:数据从父到子靠 props ,子到父靠事件。这个看似简单的约定,实际在工程中划出了一条清晰的职责边界。你不会在 React 中看到 props.onSave() 这种写法被鼓励,同理,在 Vue 里,如果子组件能随意修改父组件的响应式数据(比如直接 this.$parent.formData.name = 'xxx' ),那这个组件本质上就是“不可移植”的——它和当前父组件深度耦合,换个上下文就崩。而 $emit 强制你声明“我能对外抛出什么”,父组件也必须显式监听“我关心哪些信号”,这种显式契约,正是大型项目可维护性的基石。
你搜到的那些热词—— vue面试题 、 vue项目实战 、 vue devtools插件下载 ——背后都指向同一个现实:企业招人看的不是你会不会写 v-on:submit.prevent ,而是你能否在复杂表单、嵌套弹窗、多级联动筛选器中,用事件机制把数据流梳理得像自来水管道一样清晰。比如 vue上传pdf文件,显示缩略图,且预览功能 这个需求,子组件 <pdf-uploader> 不该自己决定缩略图渲染逻辑或预览窗口打开方式,它只负责 emit('upload-success', { file, url, thumbnailUrl }) ;父组件根据业务场景决定是存入 Vuex、触发 API 提交,还是直接塞进 <pdf-preview> 的 src 属性。这种解耦,才是 Custom Events 真正的价值所在,远超语法层面的 v-on 和 $emit 两个关键词。
2. 核心设计思路拆解:为什么是 $emit 而不是其他方案?
2.1 为什么不用 props 回传?——数据流方向的铁律
新手最容易犯的错误,是试图用 props 实现“子传父”。比如定义一个 onSuccess prop,类型是 Function ,然后在子组件里调用 this.props.onSuccess(data) 。这在技术上完全可行,但违背了 Vue 的核心设计原则。Vue 官方文档开宗明义:“Props down, events up”。 props 是单向的、只读的,它的存在意义是让父组件向下传递配置和状态快照;而事件是双向通信的唯一合规通道。一旦允许 props 承载函数并被子组件调用,你就打开了“子组件可以任意篡改父组件行为”的潘多拉魔盒。想象一下,某个 <date-picker> 组件通过 props.onChange 直接调用父组件方法,而这个方法内部又触发了另一个 API 请求——整个调用链就变成了隐式依赖,DevTools 里根本看不到数据是如何流动的,调试时只能靠 console.log 一层层扒。
提示:Vue 3 的 Composition API 中,
defineProps默认将函数 props 标记为readonly,尝试在子组件内执行props.onSubmit()会直接报运行时警告(非编译时),这是框架在强制你遵守这条铁律。
2.2 为什么不用全局事件总线(Event Bus)?——作用域失控的风险
在 Vue 2 时代,很多人用 new Vue() 创建一个空实例作为事件总线,然后 bus.$emit('data-updated') 和 bus.$on('data-updated') 。这种方式在小型项目里很“爽”,但到了中大型项目,它会迅速演变成灾难。我参与过一个电商后台系统,初期用 Event Bus 处理商品列表刷新,后来订单、库存、促销模块都开始往 bus 上发事件,最终 bus.$on 的监听器散落在二十多个文件里,没人能说清“当点击‘上架’按钮时,到底有哪些组件会被触发”。更致命的是内存泄漏:如果某个组件 mounted 时注册了监听,但 beforeDestroy 没有 off ,这个监听器会永远挂在 bus 上,每次事件触发都执行一次无用回调。Vue 3 废除了 $on/$off/$once ,正是因为它无法被 Composition API 的响应式系统有效追踪。
相比之下, $emit 是严格限定在父子组件树内的局部通信。一个 <user-card> 组件发出的 'delete-confirmed' 事件,只会被它的直接父组件捕获,不会污染全局命名空间。这种作用域隔离,让代码的可预测性大幅提升。
2.3 为什么不用 Vuex/Pinia?——杀鸡焉用牛刀的权衡
状态管理库当然能解决跨组件通信,但它的成本远高于 $emit 。Vuex 需要定义 state 、 mutations 、 actions 、 getters 四个模块,Pinia 虽然简化了,但仍需创建 store、定义 state 和 actions 。而一个简单的“子组件提交表单后,父组件关闭弹窗”需求,用 $emit 只需两行代码:
<!-- 子组件 -->
<button @click="$emit('submit', formData)">提交</button>
<!-- 父组件 -->
<modal-form @submit="handleFormSubmit" @close="isModalOpen = false" />
引入状态管理库意味着:你需要为这个弹窗单独建一个 store,定义 isModalOpen 状态和 closeModal action,再在组件里 useStore() ,最后还要处理 store 和组件本地状态的同步。对于 90% 的父子通信场景,这是典型的过度设计。Vue 官方文档明确建议:“不要为了使用 Vuex 而使用 Vuex”, $emit 就是 Vue 为你准备的、开箱即用的轻量级解决方案。
3. 核心细节与实操要点:从基础语法到工程化实践
3.1 $emit 的三种调用方式与适用场景
$emit 不是只有 this.$emit('event-name') 这一种写法,不同场景下选择不同形式,直接影响代码的健壮性和可读性。
第一种:纯事件名(无参数)
this.$emit('close')
适用于“通知型”事件,比如关闭弹窗、取消操作。父组件监听时无需处理数据,语义清晰。但要注意,这种写法在 TypeScript 中无法进行参数类型检查,如果未来需要扩展传参,就得破坏现有 API。
第二种:事件名 + 单个参数
this.$emit('input', newValue)
// 或
this.$emit('update:modelValue', newValue) // Vue 3 v-model 语法糖底层
这是最常用的形式,尤其在实现 v-model 时。 v-model 在 Vue 2 中等价于 :value="xxx" @input="xxx = $event" ,在 Vue 3 中等价于 :modelValue="xxx" @update:modelValue="xxx = $event" 。所以如果你的自定义组件要支持 v-model ,就必须 emit('update:modelValue', value) 。这里有个关键细节: $emit 的第二个参数及之后的所有参数,都会作为 v-on 监听器的 $event 值。也就是说, @update:modelValue="handleUpdate" 中的 handleUpdate 函数,其第一个参数就是 newValue 。
第三种:事件名 + 多个参数(推荐用于复杂数据)
this.$emit('file-upload', file, { size: file.size, type: file.type }, uploadId)
当需要传递多个关联数据时,强烈建议封装成一个对象,而不是多个离散参数:
// ✅ 推荐:结构清晰,易于扩展
this.$emit('file-upload', {
file,
metadata: { size: file.size, type: file.type },
uploadId,
timestamp: Date.now()
})
// ❌ 不推荐:参数顺序易错,难以维护
this.$emit('file-upload', file, file.size, file.type, uploadId, Date.now())
原因很简单:JavaScript 函数调用不检查参数个数和类型。如果某天你删掉中间一个参数,所有监听这个事件的父组件都会因 undefined 报错,且 IDE 无法提示。而对象属性是可选的,父组件可以只取自己关心的字段,比如 @file-upload="({ file, uploadId }) => doSomething(file, uploadId)" ,即使后续增加 timestamp 字段,也不会影响现有逻辑。
3.2 v-on 的监听技巧:不止是 @event="handler"
v-on 的能力远超初学者认知。它不只是绑定一个函数,而是一套完整的事件处理 DSL。
动态事件名绑定
<child-component @[eventName]="handleEvent" />
<!-- 当 eventName = 'save' 时,等价于 @save="handleEvent" -->
这个特性在构建高阶组件时非常有用。比如一个 <form-wrapper> 组件,它需要根据传入的 submitEventName prop 来决定监听哪个事件,就可以用动态绑定,避免写一堆 v-if 判断。
事件修饰符的深层用法 @click.stop.prevent 这类修饰符大家很熟悉,但有两个容易被忽略的点:
.once修饰符:事件只触发一次。适用于“初始化完成”这类一次性通知。例如<async-data-loader @loaded.once="onFirstLoad">,确保onFirstLoad不会在后续数据刷新时重复执行。.capture修饰符:在捕获阶段触发。虽然 DOM 事件默认是冒泡,但 Vue 的自定义事件是“直接触发”,不经过捕获/冒泡阶段。.capture对$emit无效,但它对原生事件(如@click.capture)有效,可用于解决某些特殊的事件委托冲突。
监听多个事件的简写
<!-- 传统写法 -->
<child @save="onSave" @cancel="onCancel" @error="onError" />
<!-- Vue 3 的 setup 语法糖写法 -->
<child v-bind="{ onSave, onCancel, onError }" />
<!-- 注意:这要求父组件的 methods 必须是响应式函数,通常在 setup() 中用 () => {} 定义 -->
3.3 Vue 2 与 Vue 3 的关键差异与迁移陷阱
虽然 $emit API 表面一致,但底层机制和最佳实践有本质区别,不注意会导致迁移失败。
Vue 2 的 this.$emit vs Vue 3 的 emit 函数 在 Vue 2 中, $emit 是实例方法,直接 this.$emit() 。在 Vue 3 的 Composition API 中, emit 是一个从 setup() 的第二个参数 context 中解构出来的函数:
export default {
setup(props, { emit }) {
const handleSubmit = () => {
emit('submit', formData.value)
}
return { handleSubmit }
}
}
更现代的 <script setup> 语法中, emit 是一个编译时宏,无需手动解构:
<script setup>
const emit = defineEmits(['submit', 'cancel'])
const handleSubmit = () => {
emit('submit', formData.value)
}
</script>
这里有个关键点: defineEmits 的参数是一个字符串数组,它不仅声明了组件可以发出哪些事件,更重要的是, 它启用了 TypeScript 的事件类型检查 。如果你在 defineEmits(['submit']) 中声明了 submit ,那么 emit('cancel') 就会报 TS 错误。这是 Vue 3 对类型安全的重大增强。
v-model 的彻底重构 Vue 2 的 v-model 是语法糖,固定绑定 value prop 和 input 事件。Vue 3 彻底解耦,允许自定义 v-model 的 prop 名和事件名:
<!-- Vue 3 中,你可以这样用 -->
<my-input v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 它等价于 -->
<my-input
:title="pageTitle" @update:title="pageTitle = $event"
:content="pageContent" @update:content="pageContent = $event"
/>
对应的子组件需要:
// 子组件
defineProps({
title: String,
content: String
})
const emit = defineEmits(['update:title', 'update:content'])
// 在需要更新时
emit('update:title', newTitle)
这个变化让组件的 API 设计更灵活,但也意味着,如果你在 Vue 2 项目中写了 v-model ,升级到 Vue 3 后,必须显式重写为 :value="xxx" @input="xxx = $event" ,或者按新规范改造子组件。
4. 实操过程详解:从零构建一个可复用的搜索组件
4.1 需求分析与组件拆解
我们以一个高频需求为例: vue评论区功能详细教程 中必然涉及的“搜索评论”功能。它不是一个简单输入框,而是一个包含以下要素的复合组件:
- 输入框(支持防抖)
- 清除按钮(X 图标)
- 搜索按钮(放大镜图标)
- 加载状态(搜索中显示 loading)
- 错误状态(网络请求失败)
- 支持键盘回车触发搜索
- 支持外部控制(父组件可调用
focus()方法)
这个组件如果写成“大而全”的单文件,很快就会变得臃肿。正确的做法是分层: <search-box> 作为 UI 层,只负责交互和样式; <search-service> 作为逻辑层,封装防抖、API 调用等;而 $emit 就是连接这两层的胶水。
4.2 <search-box> 组件完整实现
<!-- SearchBox.vue -->
<template>
<div class="search-box">
<input
ref="inputRef"
v-model="localQuery"
type="text"
class="search-input"
:placeholder="placeholder"
@keyup.enter="handleSearch"
@blur="handleBlur"
@focus="handleFocus"
/>
<!-- 清除按钮:仅在有值且未禁用时显示 -->
<button
v-if="localQuery && !disabled"
type="button"
class="clear-btn"
@click="clearQuery"
aria-label="清除搜索内容"
>
✕
</button>
<!-- 搜索按钮 -->
<button
type="button"
class="search-btn"
:disabled="loading || disabled"
@click="handleSearch"
aria-label="搜索"
>
<span v-if="!loading">🔍</span>
<span v-else>⏳</span>
</button>
</div>
</template>
<script setup>
import { ref, watch, onMounted, defineProps, defineEmits, defineExpose } from 'vue'
// Props 定义(TypeScript 类型更佳,此处用 JS 注释示意)
const props = defineProps({
modelValue: { // v-model 绑定的值
type: String,
default: ''
},
placeholder: {
type: String,
default: '搜索评论...'
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
})
// Emits 声明:明确告知外界本组件能发出哪些事件
const emit = defineEmits([
'update:modelValue', // v-model 更新
'search', // 用户主动触发搜索
'clear', // 用户点击清除
'focus', // 输入框获得焦点
'blur' // 输入框失去焦点
])
// 本地状态:避免直接修改 props.modelValue,保持单向数据流
const localQuery = ref(props.modelValue)
// 同步 props.modelValue 到 localQuery(当父组件外部修改 v-model 时)
watch(() => props.modelValue, (newVal) => {
localQuery.value = newVal
})
// 同步 localQuery 到父组件(当用户在输入框中输入时)
watch(localQuery, (newVal) => {
emit('update:modelValue', newVal)
})
// 事件处理器
const handleSearch = () => {
if (localQuery.value.trim()) {
emit('search', localQuery.value.trim())
}
}
const clearQuery = () => {
localQuery.value = ''
emit('clear')
}
const handleFocus = () => {
emit('focus')
}
const handleBlur = () => {
emit('blur')
}
// 暴露方法给父组件调用
const inputRef = ref(null)
const focus = () => {
inputRef.value?.focus()
}
defineExpose({ focus })
// 组件挂载后,如果设置了 autofocus,则自动聚焦
onMounted(() => {
if (props.autofocus) {
focus()
}
})
</script>
<style scoped>
.search-box {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.search-input {
flex: 1;
padding: 8px 12px;
border: none;
outline: none;
font-size: 14px;
}
.clear-btn, .search-btn {
background: none;
border: none;
padding: 8px 12px;
cursor: pointer;
font-size: 16px;
}
.clear-btn:hover, .search-btn:hover {
background-color: #f5f5f5;
}
.search-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
4.3 父组件集成与事件处理
<!-- CommentList.vue -->
<template>
<div class="comment-list">
<!-- 使用 v-model 语法糖 -->
<search-box
v-model="searchQuery"
placeholder="搜索用户昵称或评论内容..."
:loading="isSearching"
@search="performSearch"
@clear="resetSearch"
@focus="onSearchFocus"
/>
<!-- 评论列表 -->
<div v-if="comments.length" class="comments">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
<p v-else class="no-results">暂无评论</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import SearchBox from './SearchBox.vue'
import { searchComments } from '@/api/comment'
const searchQuery = ref('')
const comments = ref([])
const isSearching = ref(false)
// 模拟 API 调用
const performSearch = async (query) => {
isSearching.value = true
try {
const result = await searchComments(query)
comments.value = result
} catch (error) {
console.error('搜索失败:', error)
} finally {
isSearching.value = false
}
}
const resetSearch = () => {
comments.value = []
}
const onSearchFocus = () => {
console.log('搜索框已获得焦点')
}
// 页面加载时,自动聚焦搜索框
onMounted(() => {
// 这里需要访问子组件的 focus 方法
// 在 template 中给 SearchBox 添加 ref
// <search-box ref="searchBoxRef" ... />
// 然后在 setup 中:
// const searchBoxRef = ref(null)
// onMounted(() => searchBoxRef.value?.focus())
})
</script>
4.4 关键实操心得与避坑指南
心得一:永远用 ref 包裹 v-model 的绑定值 很多新手会写 v-model="searchQuery" ,其中 searchQuery 是一个普通字符串。这在 Vue 2 中可行,但在 Vue 3 的 Composition API 中, searchQuery 必须是 ref 或 reactive 的响应式对象。否则, v-model 的双向绑定会失效,因为 v-model 本质是 :modelValue="xxx" + @update:modelValue="xxx = $event" ,而 xxx = $event 要求 xxx 是一个可赋值的引用。 ref 提供了 .value 属性来实现这一点。
心得二:防抖逻辑不应放在 <search-box> 内部 你可能会想,在 handleSearch 里直接加 debounce 。但这是错误的。防抖是业务逻辑,不是 UI 逻辑。 <search-box> 只应负责“用户点了搜索按钮”,至于要不要防抖、防抖多久,应该由父组件或独立的服务层决定。否则,这个组件就失去了通用性——下一个项目需要 300ms 防抖,你就要改组件源码。正确做法是, <search-box> 发出 search 事件,父组件在 @search 的 handler 里做防抖:
// 父组件
import { debounce } from 'lodash'
const debouncedSearch = debounce((query) => {
performSearch(query)
}, 300)
// 模板中
<search-box @search="debouncedSearch" />
心得三: defineExpose 是打破黑盒的关键 <search-box> 内部有一个 inputRef ,父组件有时需要调用 focus() 。如果不暴露 focus 方法,父组件就无法控制。 defineExpose 就是 Vue 3 提供的“白名单”机制,它明确告诉外界:“我能提供哪些方法”。这比 Vue 2 的 this.$refs.xxx.focus() 更安全,因为 defineExpose 之外的方法,父组件根本访问不到。
5. 常见问题与排查技巧实录
5.1 事件监听不生效?先查这五步
这是一个高频问题,往往不是代码写错了,而是环境或理解有偏差。我整理了一个速查表,覆盖 95% 的场景:
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
@custom-event 完全没反应 |
事件名拼写错误(大小写、连字符) | 在子组件 console.log('emitting...') ,确认 $emit 是否执行;检查父组件 @custom-event 的 custom-event 是否和 $emit('custom-event') 完全一致(包括大小写) |
Vue 事件名是区分大小写的, $emit('myEvent') 和 @myevent 不匹配。统一用 kebab-case( my-event ) |
@update:modelValue 不触发 |
v-model 绑定的变量不是 ref |
console.log(typeof searchQuery) ,确认是 object (ref)而非 string |
const searchQuery = ref('') ,不能 let searchQuery = '' |
事件能触发,但 $event 是 undefined |
$emit 参数个数与监听器期望不符 |
在子组件 console.log('emit with:', arg1, arg2) ;在父组件 @event="console.log($event)" |
确保 $emit('event', data) 和 @event="handler" 中的 handler 函数签名匹配。 $emit('event', a, b) 时, handler(a, b) 会收到两个参数 |
动态事件名 @[eventName] 不生效 |
eventName 是响应式数据,但未在 setup() 中返回 |
console.log(eventName) 在模板中,看是否为 undefined |
return { eventName, ... } ,确保 eventName 是 setup() 的返回值之一 |
defineEmits 声明后, emit('xxx') 报 TS 错误 |
声明的事件名与实际 emit 的不一致 |
console.log(emit) ,看类型提示;检查 defineEmits(['a', 'b']) 中是否有 'xxx' |
在 defineEmits 数组中添加 'xxx' |
5.2 “事件丢失”的隐形杀手:异步更新与生命周期
最隐蔽的问题,是事件在组件销毁后才触发。比如一个 <async-data-loader> 组件,在 mounted 时发起 API 请求, then 中 this.$emit('loaded', data) 。但如果用户在请求返回前就导航离开了当前页面,这个组件实例已被销毁, $emit 就会静默失败,没有任何报错。
排查技巧:
- 在
beforeUnmount(Vue 3)或beforeDestroy(Vue 2)钩子中,打印日志,确认组件是否已销毁。 - 在
then回调开头加判断:if (!this || this.isUnmounted) return(Vue 3 中isUnmounted是一个 ref)。
终极解决方案: 使用 AbortController 主动取消请求,避免“幽灵回调”:
// Vue 3 Composition API
import { onBeforeUnmount, ref } from 'vue'
export default {
setup() {
const controller = ref(null)
const loadData = async () => {
controller.value = new AbortController()
try {
const res = await fetch('/api/data', {
signal: controller.value.signal
})
const data = await res.json()
// 此时组件很可能还活着
emit('loaded', data)
} catch (err) {
if (err.name !== 'AbortError') {
emit('error', err)
}
}
}
onBeforeUnmount(() => {
controller.value?.abort()
})
return { loadData }
}
}
5.3 Vue DevTools 插件的正确用法:不只是看数据
vue devtools插件下载 后,很多人只把它当“数据查看器”。其实,它是排查事件问题的利器。
关键操作:
- Events 标签页 :切换到
Events,它会实时列出所有被emit的事件,包括事件名、参数、触发时间、来源组件。如果一个事件没出现在这里,说明$emit根本没执行。 - Components 标签页 :点击左侧组件树中的某个组件,右侧会显示它的
Props和Events。Events下会列出所有defineEmits声明的事件,以及当前已注册的监听器(即@xxx)。如果这里为空,说明父组件没写监听器。 - Timeline 标签页 :开启录制,然后操作页面,它会生成一条时间线,精确到毫秒,显示每个
emit、render、updated的发生顺序。当你怀疑事件触发时机有问题(比如emit在updated之前),Timeline 是唯一的真相。
注意:Vue DevTools 对
defineEmits的支持依赖于@vue/devtools的版本。如果你用的是 Vue 3.2+,请确保 DevTools 也升级到最新版,否则可能看不到Events标签页。
5.4 从 vue面试题 到真实工程:一个经典考题的深度解析
面试官常问:“Vue 中父子组件通信有几种方式?”标准答案是 props/$emit 、 $refs 、 Event Bus 、 Vuex/Pinia 、 provide/inject 。但资深面试官会追问:“如果让你设计一个表单验证组件,你会如何用 $emit 实现验证结果的反馈?”
我的回答(基于真实项目经验): 我不会让子组件 emit('validate', { valid: true, errors: [] }) 。因为验证是异步的(比如邮箱格式校验、用户名唯一性 API 调用), $emit 是同步的,无法表达“正在验证中”的状态。我会设计三个事件:
@validate-start:验证开始,父组件可显示 loading@validate-success:验证通过,参数是{ field: 'email', value: 'a@b.com' }@validate-error:验证失败,参数是{ field: 'email', message: '邮箱已被占用' }
这样,父组件可以:
- 用
validate-start和validate-success/error控制每个字段的 loading 状态 - 用
validate-error收集所有错误,统一展示在表单顶部 - 用
validate-success触发下一步(比如提交)
这个设计体现了 $emit 的核心思想: 它不是传递“结果”,而是传递“状态变迁” 。一个健壮的事件系统,应该能描述“开始-进行中-成功/失败”这一完整生命周期,而不是只关注最终结果。
6. 最后一点个人体会:别把 $emit 当语法,要当契约
写这篇内容时,我翻出了三年前的一个项目代码库。那个 <date-range-picker> 组件,当时为了赶工期, $emit 了七个事件: change , start-change , end-change , focus , blur , clear , apply 。现在看, start-change 和 end-change 是冗余的,因为 change 事件的参数已经包含了 startDate 和 endDate 。而 apply 事件,本质上就是 change 的一个子集,完全可以合并。
这让我意识到, $emit 的数量和粒度,是组件设计水平的直接体现。一个优秀的自定义事件 API,应该像 RESTful API 一样,遵循“资源导向”: <user-card> 就应该 emit user-delete , user-edit , user-view ; <file-uploader> 就应该 emit file-add , file-remove , file-upload-start , file-upload-success 。每一个事件名,都应该是一个清晰的、动宾结构的业务动作,而不是技术动作(比如 input-change 、 click-handler )。
所以,下次你再写 $emit 时,不妨多问自己一句:“这个事件,是业务人员能听懂的吗?如果我把事件名写在 PR 描述里,同事能立刻明白它代表什么业务含义吗?” 如果答案是否定的,那就值得停下来,重新思考这个组件的职责边界和事件契约。毕竟,代码是写给人看的,只是恰好能被机器执行。
更多推荐
所有评论(0)