События во Vue
События — основной способ взаимодействия пользователя с интерфейсом и коммуникации компонентов. Во Vue за обработку событий отвечает директива v-on, сокращённо @. Она позволяет подписываться на нативные DOM-события, отправлять собственные события между компонентами и использовать декларативные модификаторы для типовых задач.
Эта статья посвящена событиям во Vue 3 с акцентом на Composition API и <script setup>. Мы разберём v-on и её модификаторы, компонентные события через emits, нативные события на компонентах, а также посмотрим, как устроен v-model под капотом. Options API упоминается как legacy-вариант, который полезно уметь читать.
Содержание
- Директива
v-on - Объект события и
$event - Модификаторы событий
- Модификаторы клавиш
- Нативные события на компонентах
- Компонентные события через
emits v-modelкак сахар для события- Несколько
v-modelи кастомные модификаторы - Паттерны коммуникации компонентов
- Глобальные события
- Лучшие практики и антипаттерны
- Сравнение с React
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Директива v-on
v-on подписывается на DOM-события. Полная форма — v-on:click="handler", сокращённая — @click="handler".
Inline-обработчик
Простые действия можно писать прямо в шаблоне:
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
Если логика разрастается, лучше вынести её в функцию.
Метод-обработчик
Если значением атрибута является имя функции, Vue вызовет её с объектом события:
<script setup>
function greet(event) {
console.log(event.target); // <button>
}
</script>
<template>
<button @click="greet">Greet</button>
</template>
Важно не вызывать функцию в шаблоне самостоятельно: @click="greet()" — это уже inline-вызов, и Vue не передаст объект события без $event.
Аргументы и $event
Если нужно передать собственные данные вместе с событием, используется $event:
<script setup>
function submit(item, event) {
console.log(item.id);
event.preventDefault();
}
</script>
<template>
<button @click="submit(product, $event)">Buy</button>
</template>
$event — это Vue-способ получить DOM-событие внутри inline-выражения. В метод-обработчике событие передаётся автоматически.
Несколько обработчиков
Несколько действий разделяются точкой с запятой:
<template>
<button @click="trackAnalytics(); increment()">Click</button>
</template>
Объект события и $event
В Vue обработчики нативных событий получают нативный DOM-объект события, а не синтетическую обёртку. Это ключевое отличие от React.
<script setup>
function logEvent(event) {
console.log(event.type); // 'click'
console.log(event.target); // элемент, инициировавший событие
console.log(event.currentTarget); // элемент, на котором висит обработчик
}
</script>
<template>
<button @click="logEvent">Log</button>
</template>
target vs currentTarget
event.target— элемент, на котором фактически произошло событие.event.currentTarget— элемент, к которому привязан обработчик.
<template>
<button @click="handleClick">
<span>Click me</span>
</button>
</template>
При клике на <span> target будет <span>, а currentTarget — <button>.
$event в inline-обработчике
<template>
<input @input="text = $event.target.value" />
</template>
Это эквивалентно:
<script setup>
const text = ref('');
function onInput(event) {
text.value = event.target.value;
}
</script>
<template>
<input @input="onInput" />
</template>
Модификаторы событий
Vue предоставляет декларативные модификаторы для типовых задач.
| Модификатор | Действие |
|---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.capture | Фаза захвата |
.self | Только если target === currentTarget |
.once | Выполнить один раз |
.passive | Не вызывать preventDefault |
Примеры:
<template>
<form @submit.prevent="handleSubmit">...</form>
<div @click="handleOuter">
<button @click.stop="handleInner">Click</button>
</div>
<div @click.capture="handleCapture">
<button @click="handleTarget">Click</button>
</div>
<div @click.self="handleDiv">
<button>Click me</button>
</div>
<button @click.once="trackFirstClick">Track once</button>
<div @scroll.passive="handleScroll">...</div>
</template>
Цепочки модификаторов
Модификаторы можно комбинировать:
<template>
<a @click.stop.prevent="handleLink">Link</a>
</template>
Порядок важен: @click.prevent.self предотвратит действие по умолчанию только при клике по самому элементу, а @click.self.prevent сначала проверит self, затем вызовет preventDefault.
Модификаторы кнопок мыши
<template>
<div @click.right="showContextMenu">Right click me</div>
</template>
Модификаторы клавиш
Для событий клавиатуры Vue предоставляет псевдонимы.
Встроенные псевдонимы
<template>
<input @keyup.enter="submit" />
<input @keyup.esc="cancel" />
<input @keyup.tab="nextField" />
</template>
Полный список: .enter, .tab, .delete (Delete и Backspace), .esc, .space, .up, .down, .left, .right.
Системные модификаторы и .exact
<template>
<input @keyup.ctrl.enter="submit" />
<button @click.shift="handleShiftClick">Shift + click</button>
<!-- только Ctrl + click, без других модификаторов -->
<button @click.ctrl.exact="handleExactCtrl">Exact Ctrl</button>
</template>
Системные модификаторы: .ctrl, .alt, .shift, .meta.
Произвольные клавиши
<template>
<input @keyup.page-down="nextPage" />
<input @keyup.arrow-up="moveUp" />
</template>
В Vue 3 рекомендуется использовать KeyboardEvent.key в kebab-case. Числовые keyCode больше не поддерживаются.
Нативные события на компонентах
Когда на компонент вешается слушатель, Vue пытается понять, является ли событие компонентным или нативным.
emits определяет пользовательские события
Если событие объявлено в defineEmits или emits, Vue считает его компонентным.
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['submit']);
function handleClick() {
emit('submit', { id: 1 });
}
</script>
<template>
<button @click="handleClick">Submit</button>
</template>
<!-- Parent.vue -->
<template>
<Child @submit="onChildSubmit" />
</template>
@submit здесь — компонентное событие, а не нативное submit формы.
Fallthrough-атрибуты
Если событие не объявлено в emits, Vue считает его нативным и добавляет слушатель на корневой элемент компонента:
<!-- BaseButton.vue -->
<template>
<button>Click me</button>
</template>
<!-- Parent.vue -->
<template>
<BaseButton @click="handleClick" />
</template>
@click автоматически пробросится на <button> внутри BaseButton.
.native удалён в Vue 3
Во Vue 2 был модификатор .native, который принудительно привязывал событие к корневому DOM-элементу. В Vue 3 он не нужен: необъявленное событие и так считается нативным.
inheritAttrs
Для полного контроля над пробросом атрибутов используется inheritAttrs: false и $attrs:
<script setup>
defineOptions({
inheritAttrs: false,
});
</script>
<template>
<div>
<button v-bind="$attrs">Click</button>
</div>
</template>
$attrs содержит все fallthrough-атрибуты, включая обработчики нативных событий.
Компонентные события через emits
Компонентные события — способ сообщить родителю, что в дочернем компоненте что-то произошло. Это аналог callback-пропсов в React.
defineEmits в <script setup>
<!-- Counter.vue -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const emit = defineEmits(['increment']);
function increment() {
count.value++;
emit('increment', count.value);
}
</script>
<template>
<button @click="increment">+</button>
</template>
<!-- Parent.vue -->
<script setup>
function onIncrement(value) {
console.log('New count:', value);
}
</script>
<template>
<Counter @increment="onIncrement" />
</template>
Валидация событий
Можно объявлять события массивом строк или объектом с валидаторами:
<script setup>
const emit = defineEmits({
submit: (payload) => payload && typeof payload.email === 'string',
cancel: null,
change: (value) => typeof value === 'number',
});
</script>
Если валидатор вернёт false, Vue выведет предупреждение в dev-режиме.
Options API
В Options API события объявляются через опцию emits, а для отправки используется this.$emit:
<script>
export default {
emits: ['submit'],
methods: {
handleSubmit() {
this.$emit('submit', { id: 1 });
},
},
};
</script>
Именование событий
Рекомендуется использовать kebab-case для имён событий в шаблонах:
<template>
<Child @update-value="handleUpdate" />
</template>
v-model как сахар для события
v-model — синтаксический сахар для связки prop + событие. По умолчанию это modelValue и update:modelValue.
Нативный input
<script setup>
import { ref } from 'vue';
const text = ref('');
</script>
<template>
<input v-model="text" />
</template>
Это эквивалентно:
<template>
<input :value="text" @input="text = $event.target.value" />
</template>
v-model на компоненте
<CustomInput v-model="text" />
Это то же самое, что:
<CustomInput
:modelValue="text"
@update:modelValue="text = $event"
/>
Реализация внутри компонента:
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
Не мутируйте props
Следующий код антипаттерн:
<script setup>
const props = defineProps(['modelValue']);
</script>
<template>
<!-- ❌ мутируем prop напрямую -->
<input v-model="props.modelValue" />
</template>
Props должны оставаться иммутабельными. Вместо мутации отправляйте событие.
Computed getter/setter
Удобный паттерн — локальный computed:
<script setup>
import { computed } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
</script>
<template>
<input v-model="localValue" />
</template>
Несколько v-model и кастомные модификаторы
Именованный v-model
<!-- Parent.vue -->
<template>
<UserForm v-model:first-name="firstName" v-model:last-name="lastName" />
</template>
Внутри компонента:
<!-- UserForm.vue -->
<script setup>
defineProps(['firstName', 'lastName']);
const emit = defineEmits(['update:firstName', 'update:lastName']);
</script>
<template>
<input :value="firstName" @input="emit('update:firstName', $event.target.value)" />
<input :value="lastName" @input="emit('update:lastName', $event.target.value)" />
</template>
Встроенные модификаторы
| Модификатор | Действие |
|---|---|
.lazy | Событие change вместо input |
.number | Приводит значение к числу |
.trim | Обрезает пробелы по краям |
<template>
<input v-model.trim="text" />
<input v-model.number="age" type="number" />
<input v-model.lazy="description" />
</template>
Кастомные модификаторы
<!-- MyInput.vue -->
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(['update:modelValue']);
function emitValue(event) {
let value = event.target.value;
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
emit('update:modelValue', value);
}
</script>
<template>
<input :value="modelValue" @input="emitValue" />
</template>
<!-- Parent.vue -->
<template>
<MyInput v-model.capitalize="title" />
</template>
То, как v-model собирается в реальную форму — defineModel, множество моделей, встроенные и кастомные модификаторы, валидация и доступность — разобрано в статье Формы и валидация во Vue.
Паттерны коммуникации компонентов
Props down, events up
Основной паттерн: родитель передаёт данные вниз через props, ребёнок сообщает о изменениях вверх через события.
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue';
const isOpen = ref(false);
</script>
<template>
<Modal :open="isOpen" @close="isOpen = false" />
<button @click="isOpen = true">Open modal</button>
</template>
<!-- Modal.vue -->
<script setup>
defineProps(['open']);
const emit = defineEmits(['close']);
</script>
<template>
<div v-if="open" class="modal">
<button @click="emit('close')">Close</button>
</div>
</template>
Event bus устарел
Во Vue 2 использовался пустой экземпляр Vue как шина событий. В Vue 3 это не работает, потому что экземпляр приложения больше не реализует паттерн Observer. Альтернативы:
provide/injectдля зависимостей.- Внешние библиотеки вроде
mitt. - Глобальное состояние через Pinia.
mitt
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { emitter } from './eventBus';
function onNotify(message) {
console.log(message);
}
onMounted(() => emitter.on('notify', onNotify));
onUnmounted(() => emitter.off('notify', onNotify));
</script>
Глобальные события
Для событий window или document подписывайтесь вручную и отписывайтесь в onUnmounted:
<script setup>
import { onMounted, onUnmounted } from 'vue';
function onResize() {
console.log('window resized');
}
onMounted(() => window.addEventListener('resize', onResize));
onUnmounted(() => window.removeEventListener('resize', onResize));
</script>
Часто такую логику выносят в composable:
// useEventListener.js
import { onMounted, onUnmounted } from 'vue';
export function useEventListener(target, event, callback) {
onMounted(() => target.addEventListener(event, callback));
onUnmounted(() => target.removeEventListener(event, callback));
}
<script setup>
import { useEventListener } from './useEventListener';
useEventListener(window, 'resize', () => console.log('resized'));
</script>
Лучшие практики и антипаттерны
- Всегда объявляйте
emits. Это делает контракт компонента понятным и помогает Vue отличать компонентные и нативные события. - Не мутируйте props. Отправляйте событие, чтобы родитель обновил состояние.
- Не пишите сложную логику в inline-обработчиках. Выносите её в методы или composables.
- Используйте модификаторы вместо ручных вызовов.
@submit.preventкороче и декларативнее, чемevent.preventDefault(). - Осторожно с event bus. Он усложняет отслеживание потока данных. Предпочитайте props/emits,
provide/injectили Pinia. - Дебаунс и троттлинг. Для частых событий (
scroll,resize,input) используйте дебаунс. - Именование событий. Используйте kebab-case в шаблонах.
- Отписывайтесь от глобальных событий. Иначе возможны утечки памяти.
Сравнение с React
Синтетические vs нативные события
В React обработчики получают SyntheticEvent. Во Vue — нативный DOM-объект.
// React
function handleClick(e) {
e.preventDefault();
console.log(e.nativeEvent);
}
<!-- Vue -->
<script setup>
function handleClick(event) {
event.preventDefault();
console.log(event);
}
</script>
Делегирование
React использует делегирование на корневом контейнере. Vue компилирует обработчики непосредственно на элементы.
Модификаторы
У React нет встроенных модификаторов. Во Vue типовые случаи покрыты декларативно.
Callback-пропсы vs emits
React: <Child onUpdate={handleUpdate} />. Vue: <Child @update="handleUpdate" />. Vue строго разделяет входящие данные и исходящие события.
Controlled inputs
React: <input value={text} onChange={(e) => setText(e.target.value)} />.
Vue: <input v-model="text" />. Под капотом — prop + событие.
Ключевые тезисы для интервью
v-onи@подписываются на события. Обработчик может быть inline-выражением или методом.- Метод-обработчик получает нативное событие; в inline-выражении для доступа к нему используется
$event. - Vue предоставляет модификаторы
.stop,.prevent,.capture,.self,.once,.passiveи модификаторы клавиш/мыши. - Компонентные события объявляются через
defineEmitsв<script setup>илиemitsв Options API. - Необъявленные события становятся fallthrough-атрибутами и привязываются к корневому DOM-элементу.
v-model— сахар для:modelValue+@update:modelValue. Именованныеv-modelиспользуют другие имена.- Props нельзя мутировать. Для двустороннего связывания используется событие
update:modelValue. - Валидация
emitsпомогает зафиксировать контракт компонента. - Во Vue обработчики работают с нативными DOM-событиями, а не SyntheticEvent.
- Глобальные события на
window/documentтребуют отписки вonUnmounted.
Заключение
События во Vue — лаконичный и декларативный механизм. v-on с модификаторами покрывает большинство типовых задач, emits делает контракт компонента явным, а v-model сокращает паттерн “prop + событие” до одной директивы.
Для собеседований важно понимать разницу между компонентными и нативными событиями, знать, почему props нельзя мутировать, и чем обработка событий в Vue отличается от React.