Vue
Связь компонентов: props, emits, provide/inject
Карточка товара в интерфейсе магазина вроде Ozon состоит из десятков вложенных компонентов: галерея, цена, кнопка в корзину, бейдж скидки. Цена приходит сверху, клик по кнопке должен дойти до корзины наверх, а текущая тема оформления нужна каждому из них на любой глубине. Если тащить тему через props сквозь пять уровней, половина компонентов получает проп, который им самим не нужен. Vue даёт три разных канала под три разные задачи, и выбор канала определяет читаемость всего дерева.
- Карточка товара в e-commerce: цена и скидка идут вниз через props, событие 'добавить в корзину' всплывает вверх через emit
- Форма регистрации: каждый input - компонент с v-model, родитель собирает значения без ручной подписки на события
- Тема оформления (светлая/тёмная): provide в корне приложения, inject в любом листе дерева без передачи через промежуточные узлы
- Дизайн-системы (PrimeVue, Vuetify): компоненты Tabs/Tab общаются через provide/inject, а не через цепочку props
Предварительные знания
- Синтаксис script setup и однофайловые компоненты (SFC)
- Понимание ref и реактивных значений
- Базовый шаблонный синтаксис: интерполяция и привязка атрибутов
Props: данные вниз
Props - это входные параметры компонента, объявляемые через defineProps. Родитель передаёт значение как атрибут, ребёнок читает его как обычную переменную. Ключевое правило: поток однонаправленный. При изменении пропа в родителе ребёнок перерисовывается, но обратно ребёнок проп менять не должен - это нарушает предсказуемость и Vue выдаёт предупреждение в консоли.
Двоеточие перед именем атрибута (`:price`) превращает его в привязку выражения: число 4990 уйдёт как number. Без двоеточия (`price="4990"`) значение уйдёт как строка "4990", и арифметика сломается.
Для значений по умолчанию при типизации через дженерик применяется withDefaults(defineProps<...>(), { discount: 0 }). Это сохраняет строгие типы и убирает проверки на undefined внутри компонента.
Почему дочерний компонент не должен напрямую мутировать значение, полученное через props?
Emits: события вверх
Если props несут данные вниз, то события несут намерения вверх. Ребёнок объявляет, какие события он умеет испускать, через defineEmits, и вызывает функцию emit при действии пользователя. Родитель подписывается через v-on (короткая запись @). Полезная нагрузка события передаётся аргументами emit и приходит в обработчик родителя.
Имя события в emit пишется в camelCase ('addToCart'), а в шаблоне родителя слушается в kebab-case (@add-to-cart). Vue выполняет это сопоставление автоматически, как и для имён компонентов.
- props — Канал вниз. Родитель -> ребёнок. Данные, которые ребёнок читает. Источник правды наверху
- emit — Канал вверх. Ребёнок -> родитель. Уведомление о действии. Решение об изменении принимает родитель
Компонент-ребёнок объявил событие как emit('updateValue', x). Как родитель слушает его в шаблоне?
v-model на компоненте через defineModel
Двусторонняя привязка на компоненте - это связка props + emit под одним именем. До Vue 3.4 приходилось вручную объявлять проп modelValue и событие update:modelValue. Макрос defineModel сворачивает обе части в один реактивный ref: запись в него испускает событие наверх, а внешнее изменение обновляет его значение.
Можно объявить несколько именованных моделей: defineModel('firstName') и defineModel('lastName'), которые родитель привязывает как v-model:first-name и v-model:last-name. Это удобно для компонентов с несколькими редактируемыми полями вроде выбора диапазона дат.
Внутри инлайн-обработчика `$event` - это нативное DOM-событие, из которого читается `$event.target.value`.
Что под капотом разворачивает v-model="x" на пользовательском компоненте в Vue 3.4+ с defineModel?
provide/inject: сквозная передача без prop drilling
Когда данные нужны компоненту глубоко в дереве, протаскивание через props на каждом уровне порождает prop drilling: промежуточные компоненты получают пропы, которые им не нужны, только чтобы передать дальше. provide объявляет значение у предка, inject достаёт его у любого потомка на любой глубине, минуя промежуточные узлы.
Передаваемое значение лучше делать реактивным (ref или reactive): тогда изменение у предка автоматически дойдёт до всех потомков. Чтобы потомки не мутировали значение напрямую, предок может предоставлять заодно функцию-апдейтер, держа источник правды у себя.
provide/inject создаёт неявную зависимость: по props видно, что компонент чего-то ждёт, а inject прячется внутри. Канал оправдан для сквозных данных (тема, локаль, текущий пользователь), но для обычной передачи между соседними уровнями props и emit остаются понятнее.
Какую проблему решает provide/inject по сравнению с передачей через props?
Связь с другими темами
Этот урок задаёт каналы коммуникации. Дальше курс показывает их применение:
- Формы и v-model — v-model на пользовательском компоненте - прямое продолжение props + emit, разобранных здесь
- Слоты — Ещё один способ связи: родитель передаёт ребёнку фрагмент разметки, а не значение
Итог
- Данные текут вниз через props, объявляемые в defineProps; ребёнок их только читает и не мутирует
- События идут вверх через defineEmits и вызов emit; родитель слушает через v-on
- v-model на компоненте - синтаксический сахар над props + emit, в Vue 3.4+ оформляется через defineModel
- provide/inject передаёт значение через любое число уровней, минуя промежуточные компоненты (решает prop drilling)
- Выбор канала: props/emit для соседних уровней, provide/inject для сквозных данных вроде темы или локали
Связанные уроки
- vue-15-forms-vmodel — v-model на компоненте через defineModel - частный случай связи props+emits, формы строятся именно на нём
- vue-16-slots — Слоты - альтернативный канал передачи: не данные, а куски шаблона от родителя к ребёнку