Олег Марков
Гайд по defineEmits на Vue 3
Введение
В современном Vue 3 одной из важных особенностей SFC (Single File Component) становится Composition API. Он предоставляет всевозможные инструменты для создания гибких реактивных интерфейсов, и одним из ключевых инструментов становится функция defineEmits. Она помогает удобно, безопасно и явно определять события, которые ваш компонент может отправлять наружу. Такой подход заметно улучшает поддержку TypeScript, читабельность кода и контроль за контрактами компонентов.
В этой статье вы разберетесь, как именно работает defineEmits, как правильно объявлять ваши события, прокидывать данные, оформлять типы, а также как использовать дополнительные возможности этого API в рамках вашего проекта.
Определение событий: зачем это нужно
Когда вы создаете компоненты во Vue, часто возникает необходимость «сообщить» родителю о каком-либо изменении. Для этого используются события (events). Раньше для объявления событий приходилось использовать опцию emits в опциях компонента или просто вызывать this.$emit внутри опций. С появлением Composition API и появился удобный способ явно и декларативно объявлять события через defineEmits.
Это решение решает сразу несколько задач:
- Дает четкую документацию — видно, какие события компонент может сгенерировать.
- Добавляет типизацию (особенно важно для TypeScript).
- Защищает от опечаток в именах событий.
- Улучшает автокомплит и интроспекцию в IDE.
Как работает defineEmits
В SFC с вы объявляете свои события через defineEmits. Эта функция возвращает функцию, с помощью которой вы впоследствии можете генерировать события. В самой сути подхода — это декларативное описание возможных событий и сигнатур их payload'ов.
Смотрите, как это выглядит в простейшем примере:
<script setup>
// Здесь мы объявляем, что наш компонент может отправлять событие "increment"
const emit = defineEmits(['increment'])
// Функция вызывается при клике, срабатывает событие
function onClick() {
// Генерирует событие "increment" без параметров
emit('increment')
}
</script>
<template>
<button @click="onClick">Прибавить</button>
</template>
В этом коде очевидно, что компонент способен сгенерировать только событие increment
.
Типизация событий с defineEmits
Особенно удобно defineEmits работает с TypeScript, позволяя детально описать payload ваших событий. Таким образом, если кто-то попытается вызвать событие несуществующем названием или передаст некорректные параметры, вы получите ошибку еще на этапе компиляции.
Давайте рассмотрим, как типизируется defineEmits — на примере:
<script setup lang="ts">
// Определяем события с помощью сигнатуры объекта
const emit = defineEmits<{
(e: 'add', value: number): void
(e: 'remove', id: string): void
}>()
function addItem() {
emit('add', 10) // Верно
// emit('add', 'строка') // Тут TypeScript скажет об ошибке!
}
function removeItem() {
emit('remove', 'item42') // Верно
// emit('remove', 100) // Тут тоже ошибка по типу!
}
</script>
Обратите внимание: с помощью TypeScript сигнатуры вы четко определяете, какие события существуют и какие параметры должны (или не должны) быть переданы.
Сравнение: declareEmits vs defineEmits в
В обычном SFC без <script setup>
раньше события определяли через опцию emits объекта компонента:
export default {
emits: ['save', 'delete']
}
Теперь в <script setup>
вам следует использовать defineEmits. Это короче и проще, к тому же интегрировано в Composition API и поддерживает реальную типизацию через TS.
Варианты объявлений events через defineEmits
Через массив названий
Это самый простой способ — просто укажите список событий, которые компонент может генерировать:
<script setup>
const emit = defineEmits(['open', 'close'])
</script>
Через объект с валидаторами
Иногда важно не только объявить событие, но и удостовериться, что оно вызывается корректно с правильными параметрами (особенно для JS, без TS).
Vue поддерживает для defineEmits объектную форму, где ключ — название события, а значение — валидатор (функция возвращает bool):
<script setup>
const emit = defineEmits({
submit: (payload) => {
// здесь payload — то, что передается в emit
// например, проверяем, что приходит число
return typeof payload === 'number'
},
clear: null // событие без параметров
})
</script>
Если валидатор вернет false, Vue при dev-сборке выдаст предупреждение.
Через сигнатуры TypeScript
Этот способ вы уже видели — он обеспечивает наилучшую интеграцию с автокомплитом и строгой типизацией.
<script setup lang="ts">
const emit = defineEmits<{
(e: 'save', data: { id: number, text: string }): void
(e: 'cancel'): void
}>()
</script>
Как использовать emit внутри компонента
После вызова defineEmits, вы получаете функцию emit. Просто вызывайте ее для отправки события.
Вот базовый пример использования emit с передачей данных:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'submit', value: string): void
}>()
function handleSubmit() {
emit('submit', 'hello world') // Здесь мы отправляем событие и строку
}
</script>
<template>
<button @click="handleSubmit">Отправить</button>
</template>
Передача событий родителям
Когда компонент отправляет событие, родитель может подписаться на это событие с помощью синтаксиса v-on (или просто через @):
<MyButton @submit="onSubmit" />
В данном случае родитель будет вызывать свой обработчик в ответ на emit('submit', ...).
Применение defineEmits в реальных сценариях
1. Классический компонент-контрол
Часто встречающийся паттерн — элемент управления (например, кастомный input или select), который сообщает родителю о изменениях значения:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
// Используем двойное связывание через v-model
defineProps<{ modelValue: string }>()
function onInput(event: Event) {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
Родительский компонент сможет использовать v-model:
<CustomInput v-model="username" />
В этом примере видна тесная интеграция defineEmits с паттернами двусторонней привязки.
2. Диалоговое окно с кастомными событиями
Можно организовать взаимодействие между диалогом и родителем:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'confirm', result: boolean): void
(e: 'cancel'): void
}>()
function confirmDialog() {
emit('confirm', true)
}
function cancelDialog() {
emit('cancel')
}
</script>
<template>
<button @click="confirmDialog">OK</button>
<button @click="cancelDialog">Отмена</button>
</template>
3. Прием и переотправка событий (event forwarding)
Иногда дочерний компонент просто «перекидывает» событие родителю, возможно, чуть поменяв параметры или имя:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'submit', data: string): void
}>()
function onChildSubmit(data: string) {
emit('submit', data)
}
</script>
<template>
<ChildComponent @submit="onChildSubmit" />
</template>
Особенности и ограничения defineEmits
- Работает только внутри
<script setup>
— если вы используете обычный скрипт, работает толькоemits:
в опциях. - Не поддерживает динамические имена событий — TypeScript проверяет только явно объявленные имена.
- Не контролирует правильность имен событий на стороне родителя — если родитель слушает несуществующее событие, это не приведет к ошибке, просто не произойдет вызов.
- Валидаторы в объектной форме не работают в production — проверки происходят только в режиме разработки.
Лайфхаки и лучшие практики
Используйте типы для сложных событий
Если событие передает сложный объект, вынести его структуру в отдельный интерфейс:
interface UserData {
id: number
name: string
}
const emit = defineEmits<{
(e: 'save', user: UserData): void
}>()
Явно именуйте события
Если событие связано с изменением значения, используйте паттерн update:modelValue, чтобы v-model корректно работал с вашим компонентом.
Не злоупотребляйте emit
Переходите к useExpose или provide/inject, если компонент начинает передавать слишком много разных событий.
Проверяйте корректность payload в рантайме
TypeScript не гарантирует вам правильную работу в runtime. Для сложных payload, если есть подозрение на внешний ввод, стоит добавить защиту — например, использовать валидаторы.
Заключение
Vue 3 с появлением defineEmits в делает API для событий максимально прозрачным, предсказуемым и типобезопасным. Вы теперь явно видите, какие события способны исходить из компонента, а ваши коллеги благодаря этому быстрее читают код и могут не бояться рефакторинга. Интеграция с TypeScript позволяет сделать dev-опыт еще приятнее и безопаснее, а объектная форма с валидаторами пригодится в JS-проектах. Следуйте лучшим практикам и пользуйтесь defineEmits в новых компонентах для надежной работы со событиями.
Частозадаваемые технические вопросы по теме defineEmits
Как пробросить все события дочернего компонента напрямую во внешний компонент?
Вы можете использовать $attrs и v-bind, чтобы пробросить переданные родителю события, не объявляя их по-отдельности:
<ChildComponent v-bind="$attrs" />
Расшифровка: так вы все входящие слушатели (например, @click, @update:modelValue) переадресуете на дочерний элемент.
Можно ли динамически определять список событий в defineEmits?
Нет, объявления событий в defineEmits статичны. Для динамических сценариев используйте массив или объектную форму, однако сигнатуры в TS строго фиксированы.
Как добавить описание события для автогенерации документации?
Сам по себе defineEmits не поддерживает описательные комментарии к событиям. Для автогенерации документации используйте jsdoc выше вызова defineEmits:
/**
* @event save - Сохраняет результат
*/
const emit = defineEmits(['save'])
И обрабатывайте с помощью сторонних инструментов.
Как быть с событиями, которые должен прокидывать mixin или composable в ?
Вызывайте defineEmits в каждом компоненте, где происходит emit. Если выносите обработку в composable, передавайте emit как аргумент:
export function useStuff(emit: ReturnType<typeof defineEmits>) {
// ...
}
Почему TypeScript не ругается на отсутствующее событие при использовании emit('unknownEvent')?
Это возможно лишь если вы указали форму без типизации (массив/объект без TS). Для строгой типизации пишите сигнатуру через TS, тогда все несуществующие события вызовут ошибку типов на этапе сборки.