Олег Марков
Директива v-model во Vue от основ до продвинутого использования
Введение
Директива v-model во Vue отвечает за двустороннюю привязку данных между состоянием приложения и пользовательским интерфейсом. Если сказать проще, она позволяет вам связать переменную в компоненте и значение в поле ввода так, чтобы любые изменения в одном сразу отражались в другом.
Смотрите, я покажу вам, как это выглядит в самом простом случае. В шаблоне вы пишете:
<input v-model="message" />
<p>{{ message }}</p>
И как только пользователь вводит текст в поле, он тут же появляется в абзаце. При этом если в коде компонента вы измените message, значение в поле ввода также обновится. Именно это и называется двусторонней привязкой.
Давайте разберемся, как работает директива v-model, какие варианты синтаксиса поддерживает Vue 2 и Vue 3, как она ведет себя с разными типами полей ввода, а также как правильно писать собственные компоненты с поддержкой v-model, чтобы ваш код оставался простым и предсказуемым.
Что такое v-model и как она работает под капотом
Основная идея v-model
v-model упрощает работу с формами. Вместо того чтобы вручную:
- читать значение из события
inputилиchange, - обновлять нужное поле в
dataилиsetup, - следить за тем, чтобы поле ввода обновлялось при изменении данных,
вы пишете одну директиву, а Vue берет рутину на себя.
В очень упрощенном виде:
для стандартных HTML-элементов v-model комбинирует:
- значение атрибута (например
valueилиchecked), - и событие (например
input,change).
- значение атрибута (например
для пользовательских компонентов v-model превращается:
- в
:modelValue="..."(или другое имя поля), - плюс
@update:modelValue="..."(или другое имя события).
- в
Сейчас вы увидите, как это работает в обоих случаях.
Эквивалент без v-model для стандартных элементов
Давайте разберемся на примере простого текстового поля во Vue 3:
<input v-model="message" />
Vue разворачивает это примерно в такую запись:
<input
:value="message" <!-- Привязка значения к состоянию -->
@input="event => message = event.target.value" <!-- Обновление состояния при вводе -->
/>
То есть:
- значение инпута берется из
message, - при каждом событии
inputпеременнаяmessageобновляется.
Здесь важно понимать теорию: v-model не делает "магии", она просто автоматизирует часто повторяющийся шаблон "связать значение + подписаться на событие".
v-model в Vue 2 и Vue 3: отличия
Общее поведение
И в Vue 2, и в Vue 3 v-model на стандартных HTML-элементах (input, select, textarea) ведет себя одинаково. Основные отличия начинаются, когда вы используете v-model на собственных компонентах.
Синтаксис для компонентов в Vue 2
В Vue 2 один компонент может поддерживать только один "основной" v-model. Для этого сам компонент должен:
- принимать проп
value, - эмитить событие
input.
Пример:
// Компонент Vue 2
Vue.component('BaseInput', {
props: ['value'], // Проп, связанный с v-model
template: `
<input
:value="value" <!-- Получаем значение от родителя -->
@input="$emit('input', $event.target.value)" <!-- Отправляем новое значение -->
/>
`
})
Использование:
<base-input v-model="username"></base-input>
Под капотом это превращается примерно в:
<base-input
:value="username"
@input="value => username = value"
></base-input>
Если вам нужно больше одной привязки, в Vue 2 используется модифицированный синтаксис:
<custom-input
v-model="value"
:checked.sync="isChecked" <!-- через .sync -->
></custom-input>
Но здесь уже нет единой схемы, а .sync часто производит путаницу.
Синтаксис для компонентов в Vue 3
В Vue 3 общий подход стал более явным и гибким. Теперь:
- по умолчанию ожидаются:
- проп
modelValue, - событие
update:modelValue,
- проп
- можно объявлять несколько v-model с разными именами.
Смотрите, как это выглядит.
Компонент:
// Компонент Vue 3
export default {
name: 'BaseInput',
props: {
modelValue: {
type: String,
default: ''
}
},
emits: ['update:modelValue'], // Явно объявляем событие
template: `
<input
:value="modelValue" <!-- Получаем значение от родителя -->
@input="$emit('update:modelValue', $event.target.value)" <!-- Отправляем новое -->
/>
`
}
Использование:
<BaseInput v-model="username" />
Под капотом:
<BaseInput
:modelValue="username"
@update:modelValue="value => username = value"
/>
Vue 3 также позволяет использовать именованные модели, о них мы поговорим чуть позже.
v-model на стандартных HTML-элементах
Теперь давайте разберем разные типы элементов формы, потому что поведение v-model меняется в зависимости от типа поля.
Текстовые поля (input type="text", textarea)
Самый простой и распространенный случай.
Пример:
<input v-model="message" />
<textarea v-model="description"></textarea>
В script-части:
export default {
data() {
return {
message: '', // Связано с полем input
description: '' // Связано с textarea
}
}
}
Под капотом Vue:
- связывает
valueполя с соответствующим свойством, - слушает событие
input, - обновляет свойство при каждом изменении текста.
Числовые поля и приведение типов
Если вы используете input type="number", по умолчанию значение в переменную попадает как строка. Тут часто возникает удивление у разработчиков, ожидали число, а получили строку.
<input type="number" v-model="age" />
data() {
return {
age: 0 // Здесь мы хотим число
}
}
Поведение:
- пользователь вводит "25",
- в
ageпопадает строка"25", а не число25.
Чтобы автоматически приводить к числу, используйте модификатор .number:
<input type="number" v-model.number="age" />
Теперь Vue будет пытаться преобразовать строку в число с помощью Number(). Если преобразование не удалось, значение останется строкой, поэтому важно это учитывать.
Чекбоксы (input type="checkbox")
Здесь поведение зависит от того, с чем вы связываете v-model.
Привязка к логическому значению
Если v-model связана с булевой переменной, чекбокс просто включает и выключает ее:
<input type="checkbox" v-model="isActive" />
<p>Активно ли - {{ isActive }}</p>
data() {
return {
isActive: false // Булевое значение
}
}
Под капотом:
checkedпривязан кisActive,- при клике значение
isActiveинвертируется.
Привязка к массиву
Если v-model связана с массивом, чекбокс добавляет или удаляет значение из этого массива.
<label>
<input type="checkbox" value="js" v-model="selectedSkills" />
JavaScript
</label>
<label>
<input type="checkbox" value="ts" v-model="selectedSkills" />
TypeScript
</label>
<p>Вы выбрали - {{ selectedSkills }}</p>
data() {
return {
selectedSkills: [] // Массив выбранных значений
}
}
Поведение:
- при установке чекбокса его
valueдобавляется в массив, - при снятии чекбокса его
valueудаляется из массива.
Vue использует строгое сравнение для поиска значения в массиве, поэтому важно следить за типами.
Радиокнопки (input type="radio")
Радиокнопки работают с одним значением из набора. Обычно их связывают со строкой:
<label>
<input type="radio" value="male" v-model="gender" />
Мужской
</label>
<label>
<input type="radio" value="female" v-model="gender" />
Женский
</label>
<p>Пол - {{ gender }}</p>
data() {
return {
gender: '' // Строка, соответствующая выбранному value
}
}
Под капотом:
- Vue присваивает в
gendervalueвыбранного радио-инпута, - устанавливает
checkedв зависимости от значенияgender.
Select (одиночный и множественный)
Одиночный выбор
<select v-model="country">
<option value="">Не выбрано</option>
<option value="ru">Россия</option>
<option value="us">США</option>
</select>
<p>Страна - {{ country }}</p>
data() {
return {
country: '' // Строка с value выбранной опции
}
}
Множественный выбор (multiple)
Если вы добавляете атрибут multiple, v-model должен ссылаться на массив.
<select v-model="countries" multiple>
<option value="ru">Россия</option>
<option value="us">США</option>
<option value="de">Германия</option>
</select>
<p>Страны - {{ countries }}</p>
data() {
return {
countries: [] // Массив значений выбранных опций
}
}
Под капотом Vue:
- собирает все выделенные опции,
- формирует массив их
value, - записывает его в
countries.
Модификаторы v-model
Модификаторы — это дополнительные "флажки", которые меняют поведение v-model. Давайте посмотрим, как они помогают в реальных задачах.
.lazy — обновление по событию change
По умолчанию v-model обновляет значение при каждом вводе символа (событие input). Иногда это бывает лишним, особенно если вы хотите сработать только после того, как пользователь "закончил" ввод.
<input v-model.lazy="username" />
Теперь:
- значение
usernameобновляется по событиюchange, - то есть когда поле теряет фокус или пользователь нажал Enter (поведение может зависеть от браузера).
Это полезно, если, например, вы хотите отправлять данные на сервер не при каждом символе, а только когда пользователь завершил редактирование.
.number — автоматическое преобразование в число
Мы уже немного коснулись этого, но давайте закрепим.
<input v-model.number="age" type="number" />
Vue будет вызывать Number() для введенного значения. Обратите внимание:
- пустая строка станет
0, Number('')дает0,- невалидное число даст
NaN.
Поэтому, если вам важно отличить "ничего не введено" от "0", лучше добавить дополнительную логику проверки в коде компонента.
.trim — обрезка пробелов по краям
.trim удаляет пробелы в начале и конце строки перед записью значения в модель.
<input v-model.trim="login" />
Поведение:
- пользователь вводит
" user ", - в
loginпопадает"user".
Эта мелочь сильно упрощает валидацию, особенно для логинов, email и других текстовых полей, где ведущие и хвостовые пробелы в 99% случаев не нужны.
v-model в компонентах во Vue 3
Теперь давайте посмотрим, как правильно использовать v-model с собственными компонентами. Это ключевая часть для реальных приложений.
Базовый компонент с v-model
Смотрите, я покажу вам самый простой пример компонента ввода текста, который поддерживает v-model.
Компонент BaseInput.vue:
<template>
<label>
<span>{{ label }}</span>
<input
:value="modelValue" <!-- Привязываем значение -->
@input="$emit('update:modelValue', $event.target.value)" <!-- Эмитим новое -->
/>
</label>
</template>
<script>
export default {
name: 'BaseInput',
props: {
modelValue: {
type: String,
default: ''
},
label: {
type: String,
default: ''
}
},
emits: ['update:modelValue'] // Объявляем событие
}
</script>
Использование в родительском компоненте:
<template>
<BaseInput
v-model="username" <!-- v-model связывает modelValue и update:modelValue -->
label="Имя пользователя"
/>
<p>Текущее имя - {{ username }}</p>
</template>
<script>
import BaseInput from './BaseInput.vue'
export default {
components: { BaseInput },
data() {
return {
username: '' // Связано с BaseInput
}
}
}
</script>
Здесь важно запомнить схему:
- в дочернем компоненте:
- проп
modelValue, - событие
update:modelValue,
- проп
- в родительском:
v-model="..."связывает все автоматически.
Несколько v-model в одном компоненте (именованные модели)
Во Vue 3 вы можете использовать несколько v-model, если, например, хотите управлять значением и состоянием "отмечено" отдельно.
Смотрите, как это выглядит.
Компонент RangeWithSwitch.vue:
<template>
<div>
<label>
<span>{{ label }}</span>
<input
type="range"
:min="min"
:max="max"
:value="modelValue" <!-- Значение диапазона -->
@input="$emit('update:modelValue', Number($event.target.value))" <!-- Новое значение -->
/>
</label>
<label>
<span>Активно</span>
<input
type="checkbox"
:checked="modelActive" <!-- Дополнительная модель -->
@change="$emit('update:modelActive', $event.target.checked)" <!-- Новое состояние -->
/>
</label>
</div>
</template>
<script>
export default {
name: 'RangeWithSwitch',
props: {
modelValue: {
type: Number,
default: 0
},
modelActive: {
type: Boolean,
default: true
},
label: String,
min: Number,
max: Number
},
emits: ['update:modelValue', 'update:modelActive'] // Два события
}
</script>
Использование:
<template>
<RangeWithSwitch
v-model="volume" <!-- Основное значение -->
v-model:active="isVolumeOn" <!-- Именованная модель -->
label="Громкость"
:min="0"
:max="100"
/>
</template>
<script>
import RangeWithSwitch from './RangeWithSwitch.vue'
export default {
components: { RangeWithSwitch },
data() {
return {
volume: 50, // Связан с modelValue
isVolumeOn: true // Связан с modelActive
}
}
}
</script>
Механика:
v-model="volume"→:modelValue+@update:modelValue,v-model:active="isVolumeOn"→:active(переопределяет имя пропа) и@update:active.
Но здесь важно: мы в компоненте используем modelActive как имя пропа. Чтобы все совпадало, можно настроить другое имя, используя опцию model или согласовать имена между шаблоном и пропами. В типичном случае в Vue 3 вы будете называть пропы так же, как суффиксы у v-model, например:
<!-- Родитель -->
<MyComponent
v-model:title="title"
v-model:visible="isVisible"
/>
// Дочерний
props: {
title: String,
visible: Boolean
},
emits: ['update:title', 'update:visible']
Этот вариант обычно проще и логичнее.
v-model в Composition API (setup)
Если вы используете Composition API, логика v-model почти не меняется. Основная разница — в том, как вы описываете пропсы и события.
Простое использование v-model в родителе с setup
Родительский компонент:
<template>
<BaseInput v-model="username" label="Имя" />
<p>{{ username }}</p>
</template>
<script setup>
import { ref } from 'vue'
import BaseInput from './BaseInput.vue'
const username = ref('') // Реф, связанный с v-model
</script>
Здесь все довольно прямолинейно: ref работает как обычная реактивная переменная, и v-model с ним прекрасно взаимодействует.
Написание компонента с v-model в setup
Теперь давайте посмотрим, как безопасно и понятно реализовать компонент с v-model внутри setup.
<template>
<input
:value="value" <!-- Используем вычисленное значение -->
@input="onInput" <!-- Обработчик ввода -->
/>
</template>
<script setup>
import { computed } from 'vue'
// Описываем входные пропсы
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
// Описываем события, которые компонент может излучать
const emit = defineEmits(['update:modelValue'])
// Создаем вычисляемое свойство, которое ведет себя как v-model внутри компонента
const value = computed({
get() {
// Возвращаем текущее значение пропа
return props.modelValue
},
set(newValue) {
// Эмитим событие для обновления в родителе
emit('update:modelValue', newValue)
}
})
// Обработчик ввода из шаблона
function onInput(event) {
// Записываем в вычисляемое свойство значение из поля ввода
value.value = event.target.value
}
</script>
Почему такой подход полезен:
valueвнутри компонента выглядит как обычная реактивная переменная,- при изменении
valueавтоматически вызываетсяemit, - родительский компонент остается единственным "источником истины" для данных.
Двусторонняя привязка и однонаправленный поток данных
Здесь важно прояснить один теоретический момент, который часто вызывает вопросы.
Где "главные" данные
При использовании v-model:
- состояние, связанное с v-model, живет в родительском компоненте,
- дочерний компонент только:
- получает значение,
- уведомляет родителя об изменениях.
То есть:
props→ вниз по дереву компонентов (read-only),emit→ вверх по дереву (уведомление об изменениях).
v-model как раз и реализует этот паттерн, но в более компактной форме.
Почему не стоит мутировать проп напрямую
В дочернем компоненте не нужно (и нельзя во Vue 3 в strict-режиме) менять пропы напрямую, например:
props.modelValue = 'новое значение' // Так делать не надо
Причина:
- пропсы должны восприниматься как "входные параметры",
- если вы начнете менять их внутри, поток данных станет непредсказуемым,
- отладка таких ситуаций становится сложной.
Именно поэтому мы используем связку props + emit и часто создаем computed-поле с геттером и сеттером, как мы сделали выше.
Распространенные ошибки и подводные камни при работе с v-model
Теперь давайте посмотрим на типичные проблемы, с которыми часто сталкиваются разработчики.
Ошибка: нет события update:modelValue в компоненте
Ситуация:
- вы добавили
v-modelк компоненту, - но внутри компонента не реализовали событие
update:modelValue.
В результате значение в родителе никогда не обновляется.
Как исправить:
- Убедиться, что в компоненте есть проп
modelValue(или другой, если вы используете именованный v-model). - Добавить
emits: ['update:modelValue']. - При изменении внутри компонента вызывать
emit('update:modelValue', новоеЗначение).
Ошибка: конфликт локального состояния и v-model
Иногда в дочернем компоненте создают свою локальную реактивную переменную, которая дублирует modelValue, но ее не синхронизируют с пропом.
Пример проблемы:
// Внутри компонента
data() {
return {
internalValue: this.modelValue // Копия пропа
}
}
Дальше компонент меняет только internalValue, а родитель об этом никогда не узнает. В итоге:
- родитель думает, что значение одно,
- компонент показывает другое.
Правильный подход:
- либо вообще не дублировать
modelValueи работать только с ним через computed со сеттером, - либо синхронизировать внутреннее состояние через
watchи событияemit.
Практический пример: компонент ввода с валидацией и v-model
Давайте посмотрим более полный пример, чтобы вы увидели, как v-model вписывается в реальную задачу с валидацией.
Задача
Сделать компонент ValidatedInput, который:
- принимает значение через v-model,
- показывает ошибку, если строка короче 3 символов,
- подсвечивает поле при ошибке.
Реализация компонента
<template>
<div class="validated-input">
<label>
<span>{{ label }}</span>
<input
:value="value" <!-- Привязка к вычисляемому значению -->
:class="{ 'input-error': hasError }" <!-- Класс ошибки -->
@input="onInput" <!-- Обработка ввода -->
/>
</label>
<p v-if="hasError" class="error">
{{ errorMessage }}
</p>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
minLength: {
type: Number,
default: 3
}
})
const emit = defineEmits(['update:modelValue'])
// Вычисляемое "прокси" для modelValue
const value = computed({
get() {
return props.modelValue
},
set(newVal) {
emit('update:modelValue', newVal)
}
})
// Состояние ошибки
const hasError = ref(false)
const errorMessage = ref('')
// Функция проверки
function validate(currentValue) {
if (currentValue.length < props.minLength) {
hasError.value = true
// Формируем понятное сообщение
errorMessage.value = `Минимальная длина - ${props.minLength} символа`
} else {
hasError.value = false
errorMessage.value = ''
}
}
// Обработчик ввода
function onInput(event) {
const newVal = event.target.value
// Сначала обновляем модель
value.value = newVal
// Затем проверяем
validate(newVal)
}
// Следим за внешними изменениями modelValue
watch(
() => props.modelValue,
(newVal) => {
validate(newVal)
},
{ immediate: true } // Вызываем проверку сразу при монтировании
)
</script>
<style scoped>
.input-error {
border-color: red; /* Подсвечиваем ошибку красной рамкой */
}
.error {
color: red; /* Текст ошибки красным */
font-size: 12px;
}
</style>
Использование компонента
<template>
<ValidatedInput
v-model="login" <!-- Связываем значение -->
label="Логин"
:min-length="3"
/>
<p>Текущий логин - {{ login }}</p>
</template>
<script setup>
import { ref } from 'vue'
import ValidatedInput from './ValidatedInput.vue'
const login = ref('')
</script>
Как видите, родительскому компоненту не нужно знать, как устроена валидация. Он просто работает с login, а ValidatedInput берет на себя все проверки и отображение ошибок. v-model делает этот интерфейс естественным и логичным.
Заключение
Директива v-model во Vue решает одну повторяющуюся задачу — двустороннюю синхронизацию состояния и пользовательского ввода. Вместо того чтобы вручную привязывать значение и подписываться на каждое событие, вы используете единый, понятный синтаксис.
Ключевые моменты, которые важно удержать:
На стандартных HTML-элементах v-model:
- привязывает
value/checked/выбранные опции, - слушает нужные события (
input,change), - обновляет связанную переменную.
- привязывает
Модификаторы
.lazy,.number,.trimпомогают:- контролировать момент обновления,
- приводить типы,
- очищать ввод от лишних пробелов.
В компонентах Vue 3 v-model:
- по умолчанию использует проп
modelValueи событиеupdate:modelValue, - поддерживает несколько моделей через именованный синтаксис
v-model:имя.
- по умолчанию использует проп
Внутри компонентов лучше использовать связку:
propsкак входные параметры,emitдля уведомлений о изменениях,computedс геттером и сеттером как удобный "мост" между ними.
Если вы будете строить компоненты вокруг этой простой схемы, формы и сложные интерфейсы во Vue станут проще в реализации и сопровождении, а ваш код останется предсказуемым и читаемым.
Частозадаваемые технические вопросы по директиве v-model
1. Как использовать v-model с вложенными объектами, чтобы избежать проблем с реактивностью?
Если вы пишете v-model="user.name", Vue нормально обновляет поле, но могут возникнуть проблемы, если изначально user не имел свойства name. Безопаснее инициализировать все нужные поля заранее:
data() {
return {
user: {
name: '', // Инициализируем все свойства
email: ''
}
}
}
Если структура может меняться динамически, лучше использовать reactive в Composition API и аккуратно добавлять поля через Object.assign или полную замену объекта, а не через прямое добавление новых свойств.
2. Как правильно использовать v-model в компоненте-обертке над сторонней библиотекой (например, datepicker)?
Обычно у таких библиотек есть свои события и API. Вам нужно:
- Принять
modelValueпропом. - При инициализации виджета передать ему
modelValue. - Подписаться на событие библиотеки (например
onChange) и внутри обработчика вызыватьemit('update:modelValue', новоеЗначение). - Добавить
watchнаmodelValue, чтобы при внешнем изменении обновлять состояние виджета.
Так вы синхронизируете внешний v-model и внутреннее состояние виджета.
3. Можно ли комбинировать v-model и классический @input на одном элементе?
Да, но нужно учитывать порядок обработки. Vue выполнит оба обработчика. Если вам нужен дополнительный код при изменении значения, лучше вынести его в обработчик и вызывать уже внутри логики v-model в компоненте. На простых элементах (input, select) безопаснее использовать @input только для побочных эффектов, не меняя напрямую привязанную переменную, чтобы не запутать поток данных.
4. Как использовать v-model вместе с Vuex или Pinia, чтобы не мутировать состояние напрямую?
С Vuex не рекомендуется привязывать v-model прямо к this.$store.state. Вместо этого:
- Создайте computed с геттером и сеттером.
- В геттере возвращайте значение из стора.
- В сеттере диспатчьте экшен или вызывайте мутацию.
Пример с Composition API и Pinia:
const store = useUserStore()
const username = computed({
get: () => store.username,
set: (val) => store.updateUsername(val) // Метод стора, который меняет состояние
})
И затем используете v-model="username".
5. Как реализовать поддержку модификаторов v-model (например .trim) в собственном компоненте?
Модификаторы не приходят автоматически в компонент. Вам нужно:
- Использовать синтаксис
v-model="value"без модификатора, а обработку делать в родителе, или - Использовать
v-modelс кастомным аргументом и передавать модификаторы через отдельные пропсы.
Например:
<MyInput v-model="text" :trim="true" />
В компоненте:
- принять проп
trim, - в обработчике ввода, если
trim === true, применитьvalue.trim()перед эмитомupdate:modelValue.
Так вы явно контролируете, какие модификации применяются к значению.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

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