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 描述里,同事能立刻明白它代表什么业务含义吗?” 如果答案是否定的,那就值得停下来,重新思考这个组件的职责边界和事件契约。毕竟,代码是写给人看的,只是恰好能被机器执行。

更多推荐