Как работает v-model и модификаторы событий?

JuniorVue.js #vue #forms #events

Вопрос

Как устроена директива v-model для двусторонней привязки и что делают модификаторы событий вроде .prevent, .stop, .once?

Короткий ответ

v-model — это синтаксический сахар, который под капотом разворачивается в привязку значения (:value) плюс обработчик события ввода (@input), обновляющий это значение. Модификаторы событий (.prevent, .stop, .once и др.) — это декларативный способ вызвать event.preventDefault(), event.stopPropagation() или ограничить срабатывание обработчика, не прописывая это вручную в теле функции.

Подробный ответ

v-model. Для обычного <input> запись:

<input v-model="text" />

эквивалентна:

<input :value="text" @input="text = $event.target.value" />

То есть v-model объединяет вывод значения в атрибут и обновление переменной при событии ввода в одну директиву. Для разных элементов Vue использует разные пары событие/атрибут: <input type="checkbox"> использует checked/change, <select> — value/change и т.д. — директива сама выбирает подходящую связку.

На пользовательских компонентах v-model разворачивается в проп modelValue + событие update:modelValue (см. вопрос про props/emits) — компонент должен объявить defineModel() (или вручную defineProps(['modelValue']) + defineEmits(['update:modelValue'])), чтобы поддержать эту привязку.

Модификаторы событий. Пишутся через точку после имени события в v-on:

  • .prevent — вызывает event.preventDefault() (например, чтобы форма не перезагружала страницу при submit).
  • .stop — вызывает event.stopPropagation(), останавливая всплытие события к родителям.
  • .once — обработчик сработает только один раз, дальше снимается автоматически.
  • .self — обработчик сработает, только если событие произошло на самом элементе, а не всплыло от дочернего.
  • .capture — навешивает обработчик в фазе перехвата (capture), а не всплытия.
  • Модификаторы клавиш (@keyup.enter, @keyup.esc) — фильтруют обработчик по конкретной клавише.

Модификаторы можно комбинировать (@click.stop.prevent) и они читаются в порядке написания слева направо, что важно, например, для .self в сочетании с другими.

Также у v-model есть свои модификаторы: .lazy (обновлять по change, а не по input), .number (приводить к числу), .trim (обрезать пробелы).

Пример

<script setup>
import { ref } from 'vue'

const email = ref('')
const submitted = ref(false)

function handleSubmit() {
  submitted.value = true
  console.log('Отправлено:', email.value)
}
</script>

<template>
  <!-- .prevent — не даём браузеру перезагрузить страницу при submit -->
  <form @submit.prevent="handleSubmit">
    <!-- v-model.trim — сахар над :value + @input, плюс обрезка пробелов -->
    <input v-model.trim="email" type="email" placeholder="Email" />
    <button type="submit">Отправить</button>
  </form>

  <!-- .stop — не даём клику всплыть к обёртке; .once — сработает один раз -->
  <div @click="console.log('клик по контейнеру')">
    <button @click.stop.once="() => console.log('клик по кнопке, только один раз')">
      Нажми меня
    </button>
  </div>

  <p v-if="submitted">Форма отправлена: {{ email }}</p>
</template>

Дополнительные вопросы

  • Как реализовать v-model на собственном компоненте с полем ввода?
  • В чём разница между модификаторами .stop и .self?
  • Что делает модификатор .sync и почему он был заменён на v-model:propName в Vue 3?