Dev48
ЯЗЫК
  • О нас
  • Услуги
  • Индустрии
  • Технологии
  • Статьи
  • Контакты
Забронировать звонок
    Главная/Статьи/Uluchshennyy v model s pomoschyu definemodel
Dev48
info@dev48.org+375 29 320-40-96WhatsApp@isky_by

© 2026 · Все права защищены.

Улучшенный v-model с помощью defineModel

Источник: Telerik Blogs•12 ноября 2026 г. в 19:48

Улучшенный v-model с помощью defineModel

Вместо трех отдельных частей для двусторонней привязки значения, defineModel позволяет реализовать этот распространенный паттерн компонентов Vue одной строкой кода.

Вместо трех отдельных частей для двусторонней привязки значения, defineModel позволяет реализовать этот распространенный паттерн компонентов Vue одной строкой кода.

В моей предыдущей статье «Основы Vue: синтаксический сахар Script Setup» мы разобрали пять макросов компилятора, которые делают работу с <script setup> такой приятной. defineModel упоминался там, но делил страницу с четырьмя другими макросами. Я также прямо сказал, что сам синтаксис v-model выходит за рамки той статьи. Этот пост — недостающая часть.

Если вы пишете компоненты Vue уже некоторое время, то знаете этот ритуал. Вам нужен пользовательский компонент ввода, который работает с v-model, поэтому вы объявляете свойство (prop) modelValue, объявляете событие (emit) update:modelValue, а затем пишете небольшое вычисляемое свойство или обработчик, чтобы связать их вместе. Это три движущиеся части для одной простой идеи. Значение поступает в ваш компонент и возвращается обратно.

defineModel сводит всё это к одной строке. Эта функция стала стабильной в Vue 3.4, и сегодня я хочу показать, как она работает, во что компилируется, как обрабатывать несколько привязок и модификаторов, а также рассказать об одной ловушке, с которой вы столкнетесь, если вас никто не предупредит.

Что такое defineModel?

defineModel — это макрос компилятора, из того же семейства, что и defineProps и defineEmits. Вы вызываете его внутри <script setup>, и он возвращает вам ref.

Этот ref особенный. Чтение из него дает вам значение, переданное родителем. Запись в него сообщает родителю о необходимости обновления. Вы никогда не обращаетесь к emit напрямую.

За кулисами компилятор превращает эту одну строку в свойство с именем modelValue и событие с именем update:modelValue. Когда вы делаете model.value = 'draft', срабатывает событие с этой строкой в качестве полезной нагрузки. v-model родителя подхватывает его и обновляет собственное состояние, которое затем передается обратно вниз в виде свойства.

Так что внутри не происходит ничего нового. Это та же пара свойства и события, которую Vue всегда использовал для v-model компонентов. Разница в том, что Vue пишет это за вас.

Как мы делали это раньше

Давайте посмотрим на обертку для текстового ввода, написанную в стиле до версии 3.4, потому что разницу легче увидеть при сравнении.

BaseInput.vue

Это работает, и многие рабочие приложения до сих пор полны компонентов, написанных именно так. В этом нет ничего плохого!

Но заметьте, сколько здесь просто лишнего кода. Имя свойства и имя события должны соответствовать соглашению, которое вы должны помнить.

И еще один важный момент: вы не можете использовать v-model на внутреннем <input>, потому что вам не разрешено изменять свойство напрямую, поэтому вы возвращаетесь к :value плюс @input.

Вы, вероятно, видели и альтернативную версию этого подхода — вычисляемое свойство с геттером и сеттером, к которому прибегают, чтобы снова иметь возможность записывать значение:

Хитро, но это всё равно четыре лишние строки и еще одна вещь, которую нужно расшифровывать каждый раз при чтении компонента.

Тот же компонент с defineModel

Вот как это выглядит теперь.

BaseInput.vue

Давайте разберем это. defineModel объявляет свойство modelValue и событие update:modelValue за один раз. Объект опций принимает настройки свойств, которые вы уже знаете из defineProps. type и required ведут себя точно так же, а у default есть нюанс, к которому мы скоро перейдем. Он также принимает две дополнительные опции, get и set, которые мы рассмотрим в разделе модификаторов.

Возвращаемое значение — это ref, доступный для записи, а значит, мы можем передать его напрямую в v-model на нативном input. Никаких :value, никаких @input, никаких вычисляемых свойств с сеттером. Когда пользователь печатает, v-model записывает значение в model, запись в model вызывает событие обновления, и родитель видит новое значение.

Родитель, который использует этот компонент, вообще не меняется; он не знает о внутренней работе компонента:

SignupForm.vue

Это та часть, которая мне больше всего нравится в этом макросе. Он меняет только то, как вы пишете дочерний компонент. В родителе ничего не меняется, поэтому каждый компонент, который уже использует ваш, продолжает работать, и вы можете проводить миграцию файл за файлом, когда вам это удобно.

Required, Default и ловушка

Вы можете пометить модель как обязательную (required), что действительно полезно для компонентов, которые не имеют смысла без значения:

Вы также можете задать значение по умолчанию:

⚠️ Помните об этом!

Значения по умолчанию в модели — это не то же самое, что значения по умолчанию в обычном свойстве. Если родитель привязывает ref, который оказывается undefined, дочерний компонент будет использовать свое значение по умолчанию 'draft', но ref родителя останется undefined. Теперь обе стороны показывают разные значения, и никто вас об этом не предупредил. Дочерний компонент думает, что статус — черновик; родитель думает, что статуса вообще нет.

В официальной документации это также отмечено, и мое личное правило простое. Если компонент собирается объявить значение по умолчанию для своей модели, я инициализирую ref родителя реальным значением, а не оставляю его пустым. Пусть родитель владеет начальным состоянием, а значение по умолчанию дочернего компонента используйте только как страховку.

Для объектов и массивов оборачивайте значение по умолчанию в функцию, так же как вы делали бы это для любого другого свойства:

Несколько привязок v-model

v-model принимает аргумент, а это значит, что один компонент может предоставлять более одной двусторонней привязки. Передайте имя свойства в качестве первого аргумента в defineModel, и вы получите отдельный ref для каждого из них. Это должна быть обычная строка, так как макрос выполняется во время сборки, поэтому переменная еще не имела бы значения для чтения.

DateRangePicker.vue

И родитель привязывает каждый из них по имени:

ReportFilters.vue

Обратите внимание на регистр. В шаблоне мы пишем v-model:start-date в kebab-case, а в дочернем компоненте объявляем defineModel('startDate') в camelCase. Vue обрабатывает преобразование для свойств так же, как и всегда, поэтому оба написания относятся к одному и тому же.

Диапазоны дат — это случай, с которым я сталкиваюсь чаще всего, наряду с ползунками min/max и блоками адресов. Всё, где компонент действительно владеет несколькими связанными значениями, и где одно свойство-объект было бы хуже. Родителю пришлось бы копировать весь объект только для того, чтобы изменить одно поле.

Честно говоря, я стараюсь не делать больше двух-трех моделей в одном компоненте. Если вы обнаружили, что их больше двух, обычно это сигнал о том, что компонент делает слишком много и его стоит разделить, или данные действительно представляют собой один объект и должны передаваться как единое целое.

Модификаторы без боли

Пользовательские модификаторы v-model — это одна из тех функций Vue, которую почти никто не использовал до версии 3.4, и я почти уверен, что причина была в том, что их настройка была утомительной. Вам нужно было объявить второе свойство с очень специфическим именем, задать ему пустое значение по умолчанию и проверять его вручную.

С defineModel вы деструктурируете возвращаемое значение на пару. Первый элемент — это ref. Второй — объект модификаторов.

SlugInput.vue

Родитель активирует их через точку:

ArticleForm.vue

Давайте разберем, что происходит. Опция set — это преобразователь, который срабатывает на выходе, прямо перед тем, как значение будет передано родителю через событие.

Внутри мы проверяем modifiers.slug, который равен true только в том случае, если родитель написал .slug при привязке. Если он есть, мы приводим текст к нижнему регистру и заменяем пробелы на дефисы. В противном случае мы передаем значение без изменений.

Обратите внимание на защиту со значением &&. Опция set срабатывает при каждой записи, включая записи, которые вы делаете самостоятельно изнутри компонента. Если где-то происходит сброс через model.value = null, трансформер попытается вызвать на нем toLowerCase и выдаст ошибку. Это крошечная защита, которая предотвращает такой сбой.

Имейте в виду, что set никогда не срабатывает при чтении модели, поэтому он ничего не делает при первом рендере. Если вам нужна такая же защита при чтении, для этого существует get.

Существует соответствующая опция get, которая выполняет преобразование на входе, когда вы читаете ref модели внутри дочернего компонента. Это удобно для очистки значения, чтобы вашему шаблону не приходилось постоянно проверять его:

SearchInput.vue

Родитель может хранить null или оставлять ref пустым, а все внутри дочернего компонента может обрабатывать значение как строку. Это мягкая версия несоответствия, о котором я предупреждал ранее, так как дочерний компонент показывает пустую строку, в то время как родитель все еще хранит null. Это гораздо проще, чем придумывать значение по умолчанию, и первое же нажатие клавиши возвращает обе стороны к согласованию.

Здесь есть одно важное предостережение.

Функция get никогда не должна изменять значение, которое пользователь ввел на самом деле. Она срабатывает при каждом чтении, поэтому любое изменение реального ввода будет конфликтовать с пользователем по мере того, как он печатает.

Пример выше безопасен, потому что ?? '' вмешивается только тогда, когда там ничего нет. Любая строка, введенная пользователем, проходит без изменений.

Допустим, вы написали get, который вместо этого округляет число до двух знаков после запятой. В демо это выглядит неплохо, но get срабатывает каждый раз, когда значение читается, поэтому он переформатирует число прямо обратно в поле ввода. Введите 12.567, и поле перезапишется прямо во время ввода.

Заполнение отсутствующего значения — это нормально. Переформатирование реального — нет.

Для именованных моделей модификаторы возвращаются из деструктуризации конкретной модели:

Напишите v-model:start-date.iso в родителе, и только startModifiers увидит { iso: true }. Объект модификаторов другой модели останется пустым.

TypeScript

Если вы работаете с TypeScript, defineModel принимает параметр типа.

Это дает вам Ref<number | undefined>, так как ничто не гарантирует, что родитель что-то передал. Сделайте его обязательным, и undefined исчезнет:

Имена модификаторов также могут типизироваться с помощью второго параметра типа в виде объединения строк поддерживаемых модификаторов:

Теперь modifiers.rounded проверяет типы, а опечатка вроде modifiers.rounde — нет. Это мелочь, но именно такая мелочь избавляет вас от 10 минут растерянности.

Когда не стоит использовать defineModel

defineModel делает двустороннее связывание настолько простым в написании, что возникает соблазн использовать его везде.

Модель имеет правильную форму, когда родитель владеет значением, а дочерний компонент представляет собой более удобный способ его редактирования. Поля ввода, селекты, переключатели, пикеры, редакторе. Дочерний компонент не имеет собственного мнения о значении, кроме того, как оно отображается и изменяется.

Форма выбрана неверно, если дочерний компонент действительно сообщает о чем-то, а не редактирует. Если компонент говорит родителю «пользователь прокрутил за эту точку» или «проверка не удалась», это событие, а не модель. Используйте defineEmits и опишите, что произошло. Двустороннее связывание в данном случае просто дает родителю кусок состояния, к которому тот не должен иметь доступа для записи.

Миграция существующего компонента

Если у вас есть кодовая база со старой структурой, замена происходит механически. Удалите запись modelValue из defineProps, удалите update:modelValue из defineEmits, удалите обработчик или вычисляемое свойство и добавьте один вызов defineModel. Затем замените :value и @input в шаблоне на обычный v-model.

Две вещи, за которыми стоит следить в процессе:

  • Другие пропсы: Если у вашего компонента были пропсы наряду с modelValue, оставьте вызов defineProps для них и просто удалите запись модели. defineModel и defineProps прекрасно уживаются в одном файле, они просто не могут объявлять одно и то же имя пропа дважды.
  • Другие места вызова emit: Если вы испускали событие update:modelValue откуда-то еще, кроме обработчика ввода, например из метода сброса, это превращается в простое присваивание вида model.value = ''.

И последнее перед началом. defineModel доступна в версии 3.4 и выше, и структура деструктуризации модификаторов является частью этого же релиза. Если вы по какой-то причине застряли на старой версии 3.x, версия с пропсами и emit все еще остается полностью валидным Vue, и нет никакой срочности что-либо переписывать. Это функция для удобства разработки, а не исправление корректности.

Заключение

defineModel берет самый распространенный паттерн компонентов во Vue — значение с двусторонним связыванием — и предоставляет для него одну строку синтаксиса вместо трех отдельных частей. Вы получаете доступный для записи ref, который можно передать прямо в v-model, уже знакомые вам опции пропсов, именованные модели для компонентов с несколькими привязками и поддержку модификаторов, которой наконец-то стоит пользоваться.

И справочник по API defineModel, и руководство по v-model компонентов содержат больше подробностей, особенно в отношении опций трансформеров.

Приятного использования v-model!

← Все статьи

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →
Инференс в реальном времени для архитектур мировых моделей JEPA с использованием Ray
Anyscale

Инференс в реальном времени для архитектур мировых моделей JEPA с использованием Ray

Форум PARCEL 27
Descartes

Форум PARCEL 27

Khan Academy в партнерстве с PhET предоставляет бесплатные научные симуляции для вашего класса
Khan Academy

Khan Academy в партнерстве с PhET предоставляет бесплатные научные симуляции для вашего класса

Выбирайте инструменты аналитики электронной коммерции на основе вопросов, а не функций
Woocommerce

Выбирайте инструменты аналитики электронной коммерции на основе вопросов, а не функций

L3Harris демонстрирует морскую функциональную совместимость с системой связи FleetLink в ходе испытаний ВМС США
L3Harris

L3Harris демонстрирует морскую функциональную совместимость с системой связи FleetLink в ходе испытаний ВМС США

Руководство по миграции: как перейти с MongoDB® на Couchbase
Couchbase

Руководство по миграции: как перейти с MongoDB® на Couchbase

Ещё от Telerik

Создание заслуживающего доверия интерфейса ИИ
Telerik

Создание заслуживающего доверия интерфейса ИИ

Готовность к .NET 11: поддержка с первого дня во всем портфолио Telerik
Telerik

Готовность к .NET 11: поддержка с первого дня во всем портфолио Telerik

Представляем Server Components в TanStack Start
Telerik

Представляем Server Components в TanStack Start

QR-коды и штрихкоды: реальные сценарии использования в Blazor
Telerik

QR-коды и штрихкоды: реальные сценарии использования в Blazor

Мои любимые расширения цветов из .NET MAUI Community Toolkit
Telerik

Мои любимые расширения цветов из .NET MAUI Community Toolkit

Итак, «ИИ-дифференциатор» — это вкус... Но ЧТО ТАКОЕ вкус?
Telerik

Итак, «ИИ-дифференциатор» — это вкус... Но ЧТО ТАКОЕ вкус?