Олег Марков
Директива v-on во Vue.js - полное руководство
Введение
Директива v-on во Vue.js отвечает за обработку событий в шаблоне. Через нее вы "подписываете" элементы на клики, ввод текста, нажатия клавиш, отправку формы и многие другие события браузера и компонентов.
Смотрите, я покажу вам, как это работает: вы просто указываете событие и метод, который должен выполниться, когда это событие произойдет. Vue сам свяжет разметку и JavaScript-код и будет корректно вызывать нужные обработчики.
В этой статье мы разберем:
- базовый синтаксис v-on;
- сокращенную запись и динамические имена событий;
- работу с аргументом события event;
- модификаторы (stop, prevent, once, capture и другие);
- обработку клавиатурных и мышиных событий;
- использование v-on с компонентами и кастомными событиями;
- типичные ошибки и лучшие практики.
Теперь давайте перейдем к подробному разбору.
Базовый синтаксис v-on
Простейший пример использования
Начнем с самого простого варианта. Вот базовый шаблон с кнопкой:
<div id="app">
<!-- Привязываем клик к методу handleClick -->
<button v-on:click="handleClick">
Нажми меня
</button>
</div>
<script>
const app = Vue.createApp({
data() {
return {
count: 0 // Счетчик кликов
}
},
methods: {
handleClick() {
// При каждом клике увеличиваем счетчик
this.count++
console.log('Текущее значение count:', this.count)
}
}
})
app.mount('#app')
</script>
Как видите, директива v-on:click связывает событие click на кнопке с методом handleClick из секции methods компонента.
Vue автоматически:
- навешивает слушатель события;
- вызывает handleClick при клике;
- подставляет правильный this, ссылающийся на экземпляр компонента.
Сокращенная запись @
В повседневной разработке почти всегда используют сокращенную запись:
<!-- Полная запись -->
<button v-on:click="handleClick">Клик</button>
<!-- Сокращенная запись -->
<button @click="handleClick">Клик</button>
Сокращение @ полностью равнозначно v-on. Вы можете использовать любой вариант, но в документации и коде команд чаще всего встречается @.
Вызов методов с аргументами
Иногда вам нужно передать в обработчик дополнительные данные. Тогда вы вызываете метод как обычную функцию:
<div id="app">
<!-- Передаем статический аргумент 42 -->
<button @click="handleClick(42)">
Передать число 42
</button>
<!-- Передаем значение из данных -->
<button @click="handleClick(count)">
Передать текущий count
</button>
</div>
<script>
const app = Vue.createApp({
data() {
return {
count: 10 // Некоторое начальное значение
}
},
methods: {
handleClick(value) {
// Здесь value приходит из шаблона
console.log('Получили значение:', value)
}
}
})
app.mount('#app')
</script>
Важно: когда вы пишете @click="handleClick", Vue сам создаст функцию-обертку и вызовет handleClick без круглых скобок. Когда вы пишете @click="handleClick(42)", вы уже описываете выражение, которое Vue выполнит при событии.
Использование inline-выражений
Вы можете прямо в директиве писать простые выражения без отдельного метода:
<button @click="count++">
Увеличить count
</button>
<button @click="message = 'Обновлено'">
Обновить сообщение
</button>
Такие inline-выражения хорошо подходят для очень простых действий.
Однако, если логика начинает разрастаться:
- несколько операций,
- условные блоки,
- работа с API,
лучше выносить код в отдельный метод. Тогда шаблон будет читабельнее.
Аргумент события event и передача $event
Почему нужен объект события
Браузер для каждого события создает объект события (MouseEvent, KeyboardEvent и т. д.). Через него можно узнать:
- координаты клика;
- нажатую клавишу;
- целевой элемент;
- стандартное поведение и т. п.
Vue передает объект события в обработчик автоматически, если вы укажете его параметром.
Получение event без дополнительных аргументов
Давайте посмотрим, как это выглядит:
<button @click="handleClick">
Посмотреть объект события
</button>
<script>
const app = Vue.createApp({
methods: {
handleClick(event) {
// event - стандартный объект события браузера
console.log('Тип события:', event.type)
console.log('Кнопка была нажата на элементе:', event.target)
}
}
})
app.mount('#app')
</script>
Если обработчик объявлен как handleClick(event), Vue передаст туда объект события.
Использование $event при передаче своих аргументов
Когда вы добавляете собственные аргументы, вам нужно явно передать объект события с помощью $event:
<button @click="handleClick('left', $event)">
Кликнуть
</button>
<script>
const app = Vue.createApp({
methods: {
handleClick(side, event) {
// side - наш дополнительный аргумент
// event - объект события
console.log('Сторона:', side)
console.log('Координата по X:', event.clientX)
}
}
})
app.mount('#app')
</script>
Смотрите, здесь я использую $event, чтобы Vue передал фактический объект события вторым параметром. Это важный шаблон, когда вам нужно одновременно:
- свои данные (id, тип, индекс);
- и доступ к событию.
Модификаторы директивы v-on
Модификаторы позволяют изменить стандартное поведение события прямо в шаблоне, не лезя в код обработчика. Vue добавляет к v-on набор "сахара", который соответствует типичным операциям с event.
Синтаксис модификаторов:
<button @click.stop.prevent="handleClick">
Кнопка
</button>
Модификаторы пишутся после события через точку. Они применяются в том порядке, в каком указаны.
.stop — остановка всплытия события
Всплытие — это когда событие, произошедшее на дочернем элементе, поднимается вверх по DOM-дереву. Иногда это поведение нужно остановить.
<div @click="onParentClick">
<button @click.stop="onChildClick">
Клик по кнопке
</button>
</div>
<script>
const app = Vue.createApp({
methods: {
onParentClick() {
// Сработает, если кликнуть по div, но не по кнопке
console.log('Клик по контейнеру')
},
onChildClick() {
// Сработает при клике по кнопке
console.log('Клик по кнопке')
}
}
})
app.mount('#app')
</script>
Комментарий: благодаря .stop клик по кнопке не всплывает до div, и onParentClick не вызывается.
.prevent — отмена стандартного поведения
Иногда нужно отменить стандартное поведение элемента: переход по ссылке, отправку формы и т. д.
<a href="https://example.com" @click.prevent="onLinkClick">
Не переходить по ссылке
</a>
<script>
const app = Vue.createApp({
methods: {
onLinkClick() {
// Ссылка не откроется, но код выполнится
console.log('Стандартный переход отменен')
}
}
})
app.mount('#app')
</script>
То же самое чаще всего используют с формами:
<form @submit.prevent="onSubmit">
<!-- Здесь поля формы -->
<button type="submit">Отправить</button>
</form>
<script>
const app = Vue.createApp({
methods: {
onSubmit() {
// Форма не перезагрузит страницу
console.log('Обработка данных формы в Vue')
}
}
})
app.mount('#app')
</script>
.stop + .prevent вместе
Можно комбинировать:
<button @click.stop.prevent="handleClick">
Не всплывает и не выполняет стандартное действие
</button>
Порядок модификаторов влияет на то, в какой последовательности Vue вызовет event.stopPropagation() и event.preventDefault(). Обычно это не критично, но знать об этом полезно.
.capture — использование фазы перехвата
По умолчанию слушатели событий срабатывают на фазе всплытия. Модификатор .capture переводит обработчик на фазу перехвата (capture), то есть событие будет поймано при "спуске" сверху вниз.
<div @click.capture="onParentClick">
<button @click="onChildClick">
Клик
</button>
</div>
В этом примере onParentClick будет выполняться раньше onChildClick.
Это пригодится, когда вам нужно перехватывать события до того, как они попадут во вложенные обработчики.
.self — обработчик только для самого элемента
Модификатор .self говорит: вызывать обработчик только если событие произошло непосредственно на этом элементе, а не всплыло из дочернего.
<div @click.self="onSelfClick">
<button>
Вложенная кнопка
</button>
</div>
<script>
const app = Vue.createApp({
methods: {
onSelfClick() {
// Сработает только при клике по самому div, а не по кнопке
console.log('Клик именно по div')
}
}
})
app.mount('#app')
</script>
Здесь я размещаю пример, чтобы вам было проще понять: даже если клик по кнопке всплывает на div, обработчик с .self игнорирует такое событие.
.once — однократное срабатывание обработчика
Если вы хотите, чтобы событие обработалось только один раз, используйте .once:
<button @click.once="handleOnce">
Кликни только один раз
</button>
<script>
const app = Vue.createApp({
methods: {
handleOnce() {
// Этот код выполнится только при первом клике
console.log('Обработчик сработал один раз')
}
}
})
app.mount('#app')
</script>
Vue сам удалит слушатель после первого выполнения.
.passive — оптимизация прокрутки
Модификатор .passive говорит браузеру, что обработчик не будет вызывать preventDefault() для этого события. Это позволяет оптимизировать прокрутку, особенно на мобильных устройствах.
<div @scroll.passive="onScroll">
<!-- Содержимое с прокруткой -->
</div>
Важно: нельзя сочетать .passive и .prevent для одного и того же события, так как они противоречат друг другу.
Порядок модификаторов
Vue обрабатывает модификаторы примерно в таком порядке:
- .capture
- .once
- .passive
- .stop
- .prevent
- .self
- модификаторы клавиш (о них чуть позже)
Это значит, что сначала настраивается способ регистрации обработчика (capture, once, passive), а затем выполняются действия над event (stop, prevent и т. д.).
Обработка клавиатурных событий и модификаторы клавиш
Работа с клавиатурой — одна из самых частых задач с v-on. Vue добавляет удобные модификаторы для этого.
Базовый пример keyup и keydown
Давайте разберемся на простом примере:
<input
type="text"
@keyup="onKeyUp"
/>
<script>
const app = Vue.createApp({
methods: {
onKeyUp(event) {
// Выводим название нажатой клавиши
console.log('Нажата клавиша:', event.key)
}
}
})
app.mount('#app')
</script>
Здесь обработчик вызывается на каждое отпускание клавиши.
Модификаторы по имени клавиши
Vue позволяет написать модификатор с именем клавиши:
<input
type="text"
@keyup.enter="onEnter"
/>
<script>
const app = Vue.createApp({
methods: {
onEnter(event) {
// Сработает только при нажатии Enter
console.log('Нажат Enter, значение ввода:', event.target.value)
}
}
})
app.mount('#app')
</script>
Наиболее часто используются:
- .enter
- .tab
- .delete (для Backspace и Delete)
- .esc
- .space
- .up
- .down
- .left
- .right
Модификатор фильтрует события, вызывая обработчик только для нужной клавиши.
Модификаторы сочетаний клавиш (Ctrl, Alt, Shift, Meta)
Вы можете написать несколько модификаторов подряд, чтобы отреагировать на сочетание:
<div @keyup.ctrl.enter="save">
<!-- Здесь может быть форма или текст -->
</div>
<script>
const app = Vue.createApp({
methods: {
save() {
// Сработает только на Ctrl + Enter
console.log('Сохраняем данные')
}
}
})
app.mount('#app')
</script>
Доступны модификаторы:
- .ctrl
- .alt
- .shift
- .meta (Cmd на macOS или Win на Windows)
Vue проверит, чтобы во время события была нажата соответствующая модификационная клавиша.
Системные модификаторы и поведение по умолчанию
По умолчанию, когда вы пишете, например, @keyup.ctrl, обработчик вызовется при нажатии любой клавиши вместе с Ctrl.
Если вам нужно использовать, к примеру, сочетание Ctrl + S для сохранения и при этом отменить стандартное действие (сохранение страницы браузером), вы можете добавить .prevent:
<div @keydown.ctrl.s.prevent="save">
<!-- Зона, где отлавливаем сочетание -->
</div>
Обратите внимание, как этот фрагмент кода решает задачу:
- событие отфильтровано по Ctrl и S;
- cancel стандартного сохранения страницы;
- запуск вашей функции save.
Пользовательские модификаторы клавиш
Во Vue 3 вы можете добавить свои алиасы клавиш при создании приложения:
// Добавляем пользовательский алиас для клавиши F2
const app = Vue.createApp({ /* настройки */ })
// Здесь мы объявляем собственный модификатор .f2
app.config.keyCodes = {
f2: 113 // Код клавиши F2
}
После этого вы сможете писать:
<input @keyup.f2="onF2" />
Комментарий: в современной версии Vue основной способ — использовать event.key, но иногда такие алиасы упрощают жизнь.
Обработка событий мыши и их модификаторы
Vue также дает модификаторы для кнопок мыши.
Модификаторы кнопок мыши
Вы можете указать, какая именно кнопка должна быть нажата:
- .left
- .right
- .middle
<div
@click.left="onLeftClick"
@click.right.prevent="onRightClick"
@click.middle="onMiddleClick"
>
Кликните по области разными кнопками мыши
</div>
<script>
const app = Vue.createApp({
methods: {
onLeftClick() {
console.log('Левая кнопка')
},
onRightClick() {
// Правый клик, при этом контекстное меню отменено из-за .prevent
console.log('Правая кнопка')
},
onMiddleClick() {
console.log('Средняя кнопка')
}
}
})
app.mount('#app')
</script>
Такой подход особенно полезен для реализации контекстных меню, дополнительных действий и др.
Пример: контекстное меню на правый клик
Покажу вам, как это реализовано на практике:
<div @click.right.prevent="openContextMenu($event)">
Правый клик откроет ваше контекстное меню
</div>
<script>
const app = Vue.createApp({
data() {
return {
contextVisible: false,
contextX: 0,
contextY: 0
}
},
methods: {
openContextMenu(event) {
// Запоминаем позицию курсора
this.contextX = event.clientX
this.contextY = event.clientY
this.contextVisible = true
console.log('Контекстное меню открыто в точке:', this.contextX, this.contextY)
}
}
})
app.mount('#app')
</script>
Здесь мы используем:
- .right для правой кнопки;
- .prevent, чтобы не показывать системное контекстное меню;
- $event, чтобы получить координаты клика.
Динамические имена событий
Иногда имя события нужно вычислять динамически, например, хранить его в data или props. Vue позволяет передавать имя события как выражение:
<button v-on:[eventName]="handle">
Динамическое событие
</button>
Смотрите, как это выглядит полностью:
<div id="app">
<button v-on:[eventName]="handle">
Нажмите
</button>
<select v-model="eventName">
<option value="click">click</option>
<option value="dblclick">dblclick</option>
<option value="mouseenter">mouseenter</option>
</select>
</div>
<script>
const app = Vue.createApp({
data() {
return {
eventName: 'click' // Имя события, к которому привязан обработчик
}
},
methods: {
handle() {
console.log('Сработало событие:', this.eventName)
}
}
})
app.mount('#app')
</script>
Теперь вы увидите, как это выглядит в коде: когда вы меняете значение eventName через select, Vue переназначает обработчик на новое событие.
Важно: модификаторы (например, .stop, .prevent) нельзя делать динамическими, они всегда должны быть явно прописаны.
v-on и компоненты: работа с кастомными событиями
События компонентов и $emit
В компонентах Vue события — основной способ общаться "снизу вверх": от дочернего компонента к родителю.
На дочернем компоненте вы вызываете this.$emit('имя-события', данные), а родитель "слушает" это событие через v-on.
Пример: дочерний компонент с кнопкой
// Дочерний компонент
const ChildButton = {
template: `
<button @click="notifyParent">
Сообщить родителю
</button>
`,
methods: {
notifyParent() {
// Вызываем кастомное событие 'child-click'
this.$emit('child-click', 'Привет от дочернего компонента')
}
}
}
Пример: родитель, который слушает событие
<div id="app">
<!-- Слушаем кастомное событие child-click через v-on -->
<child-button @child-click="onChildClick" />
</div>
<script>
const app = Vue.createApp({
components: {
ChildButton
},
methods: {
onChildClick(message) {
// message - это аргумент, переданный в $emit
console.log('Сообщение от дочернего компонента:', message)
}
}
})
app.mount('#app')
</script>
Как видите, с точки зрения родителя кастомное событие никак не отличается от нативного — вы просто указываете его имя в v-on (или в сокращенной форме @).
Имена событий и стиль написания
В Vue принято использовать kebab-case для имен событий, например:
- user-selected
- item-removed
- form-submitted
На уровне кода и шаблонов это дает единообразие и читаемость:
<user-item @user-selected="handleUserSelected" />
Важно: на уровне JavaScript (в $emit) Vue не меняет регистр, поэтому имя события должно совпадать:
this.$emit('user-selected', userId)
Если вы напишете в шаблоне @userSelected, событие не поймается.
Передача нескольких аргументов в $emit
Вы можете передавать несколько значений:
this.$emit('user-selected', userId, userName)
<user-item @user-selected="onUserSelected" />
methods: {
onUserSelected(id, name) {
console.log('ID пользователя:', id)
console.log('Имя пользователя:', name)
}
}
Vue просто передает все аргументы $emit дальше в ваш обработчик.
v-on с компонентами и модификаторы
Иногда вам нужно использовать модификаторы с событиями компонентов, например .once:
<child-button @child-click.once="onOnceClick" />
В этом случае обработчик будет привязан к кастомному событию, но выполнится только один раз.
Однако .native (во Vue 2) для прослушивания "нативных" событий на корневом элементе компонента во Vue 3 убран, и вместо него рекомендуется:
- перекидывать события наружу через $emit;
- или использовать defineEmits / emits для явной декларации событий.
Отличия v-on во Vue 2 и Vue 3 (кратко)
Здесь я отмечу только те моменты, которые могут вызвать вопросы при миграции.
Директива .native
Во Vue 2:
<child-component @click.native="onClick" />
Так вы слушали нативный click на корневом элементе ребенка.
Во Vue 3 .native убрали. Теперь:
- либо ребенок сам делает $emit('click');
- либо родитель вешает обработчик на конкретный DOM-элемент снаружи, а не на компонент;
- либо вы используете v-on="$attrs" в дочернем компоненте, чтобы пробросить обработчики.
Объявление событий через emits
Во Vue 3 принято явно объявлять список событий, которые компонент может эмитить:
const ChildButton = {
emits: ['child-click'],
methods: {
notifyParent() {
this.$emit('child-click')
}
}
}
Для использования v-on на стороне родителя это ничего не меняет, но улучшает типизацию и статический анализ.
Организация кода обработчиков и лучшие практики
Держите шаблон простым
Чем проще выражения в v-on, тем легче поддерживать код. Старайтесь:
- не писать длинные цепочки логики в @click;
- выносить все, что сложнее простого присваивания, в методы.
Плохо:
<button
@click="
count++;
if (count > 10) {
showMessage('Слишком много');
}
"
>
Плохо читаемый обработчик
</button>
Лучше:
<button @click="handleClick">
Более чистый код
</button>
methods: {
handleClick() {
this.count++
if (this.count > 10) {
this.showMessage('Слишком много')
}
}
}
Используйте модификаторы вместо явного event.stopPropagation()
Вместо:
<button @click="handleClick">
<!-- Внутри handleClick вы вызываете event.stopPropagation() -->
</button>
Лучше:
<button @click.stop="handleClick">
Код в методе не касается всплытия
</button>
Так поведение видно прямо в шаблоне, а обработчик остается чисто "бизнесовым".
Не привязывайтесь к глобальному this
Не используйте стрелочные функции в methods при работе с v-on, чтобы не потерять контекст:
methods: {
// Плохо - стрелочная функция меняет контекст this
handleClick: () => {
console.log(this.count) // this не будет ссылаться на компонент
}
}
Всегда оставляйте обычный синтаксис методов:
methods: {
handleClick() {
console.log(this.count) // Это корректный this
}
}
Старайтесь не смешивать v-on с чистым addEventListener на одних и тех же элементах
Если вы добавляете слушатели через JavaScript вручную и одновременно через v-on, отладка стань сложнее. Лучше придерживаться одного подхода.
Если все события связаны с состоянием компонента, v-on — предпочтительный вариант.
Заключение
Директива v-on — основной инструмент работы с событиями во Vue.js. С ее помощью вы:
- связываете разметку и методы компонента;
- контролируете поведение событий через модификаторы;
- удобно обрабатываете клавиатурные и мышиные события;
- слушаете и обрабатываете кастомные события компонентов;
- пишете более предсказуемый и читаемый код.
Ключевые моменты, которые особенно важно запомнить:
- @ — это сокращение для v-on;
- объект события передается либо первым параметром обработчика, либо через $event;
- модификаторы .stop, .prevent, .once, .self, .capture и .passive помогают избежать лишнего кода в методах;
- для клавиатуры используйте модификаторы клавиш и модификаторов Ctrl, Alt, Shift, Meta;
- для компонентов события создаются через $emit и слушаются через v-on с именами в kebab-case.
Если вы уверенно чувствуете себя с v-on, работа с событийнной моделью во Vue становится значительно проще и предсказуемее.
Частозадаваемые технические вопросы по теме статьи и ответы на них
Как удалить обработчик события, назначенный через v-on?
Vue сам снимает слушатели при уничтожении компонента. Если вам нужно "отключить" обработчик в рантайме, используйте условный рендеринг или динамическое выражение:
<button v-if="enabled" @click="handleClick">Активно</button>
<button v-else>Не активно</button>
Либо:
<button v-on="enabled ? { click: handleClick } : {}">
Кнопка
</button>
Так вы управляете тем, будет ли обработчик вообще навешан.
Как повесить один и тот же обработчик на много разных событий?
Используйте объектную форму v-on на корневом элементе компонента:
<div v-on="listeners">
<!-- содержимое -->
</div>
data() {
return {
listeners: {
click: this.handle,
mouseenter: this.handle
}
}
},
methods: {
handle(event) {
console.log('Сработало событие:', event.type)
}
}
Изменяя объект listeners, вы можете гибко управлять набором событий.
Как правильно тестировать обработчики событий, повешенные через v-on?
В юнит-тестах (например, с Vue Test Utils) используйте методы trigger:
const wrapper = mount(MyButton)
await wrapper.find('button').trigger('click')
// Проверяете эффекты: изменение данных, вызов emit и т. д.
Если нужно протестировать модификаторы клавиш, укажите опции:
await input.trigger('keyup', { key: 'Enter' })
Так вы эмулируете реальное событие браузера.
Как сделать "дебаунс" или "троттлинг" для обработчика события?
Подключите debounce/throttle-функцию (например, из lodash) и оберните обработчик при создании:
import debounce from 'lodash/debounce'
export default {
data() {
return { value: '' }
},
created() {
this.debouncedInput = debounce(this.onInput, 300)
},
methods: {
onInput(event) {
this.value = event.target.value
}
}
}
<input @input="debouncedInput" />
Так каждый ввод будет обрабатываться не чаще одного раза в 300 мс.
Как повесить обработчик на событие window или document с помощью Vue-подхода?
Создайте обработчики в lifecycle-хуках:
export default {
mounted() {
// Вешаем слушатель на window
window.addEventListener('resize', this.onResize)
},
beforeUnmount() {
// Обязательно снимаем слушатель
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize() {
console.log('Размер окна изменился')
}
}
}
v-on в шаблоне здесь не используется, но вы продолжаете работать в духе Vue, контролируя жизненный цикл обработчиков через хуки.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев