События и слоты
Замыкаем общение компонентов: ребёнок сообщает наверх событиями через defineEmits, а родитель вкладывает разметку внутрь ребёнка через слоты.
Событие компонента — способ дочернему компоненту сообщить родителю, что что-то произошло. Слот — «окно» в компоненте, куда родитель вставляет свою разметку.
События: defineEmits
Пропсы передают данные вниз, события — сигналы вверх. Ребёнок объявляет свои события через defineEmits и испускает их функцией emit:
<!-- LikeButton.vue -->
<template>
<button @click="emit('like')">Нравится</button>
</template>
<script setup>
const emit = defineEmits(['like'])
</script>
Родитель слушает событие через @ (тот же v-on, что для DOM-событий):
<!-- App.vue -->
<template>
<p>Лайков: {{ likes }}</p>
<LikeButton @like="likes++" />
</template>
<script setup>
import { ref } from 'vue'
import LikeButton from './components/LikeButton.vue'
const likes = ref(0)
</script>
Событие с данными
В emit можно передать аргумент — он придёт в обработчик родителя:
<!-- внутри ребёнка -->
<button @click="emit('select', item.id)">Выбрать</button>
<!-- у родителя -->
<ProductItem @select="onSelect" />
<!-- function onSelect(id) { ... } -->
Модель «вверх событием» на JS
Связь «ребёнок зовёт колбэк родителя» — это передача функции вниз и вызов её наверх. По сути событие так и работает:
// Родитель даёт ребёнку обработчик
let likes = 0;
function onLike() {
likes = likes + 1;
console.log("Родитель: лайков теперь", likes);
}
// Ребёнок "испускает событие" — вызывает переданный обработчик
function childEmit(handler) {
console.log("Ребёнок: кнопку нажали");
handler();
}
childEmit(onLike);
childEmit(onLike);
Вывод:
Ребёнок: кнопку нажали Родитель: лайков теперь 1 Ребёнок: кнопку нажали Родитель: лайков теперь 2
Слоты: вставка разметки
Пропсы передают данные, а слоты — целую разметку. Компонент-обёртка определяет, где разместить содержимое тегом <slot>, а родитель кладёт это содержимое между открывающим и закрывающим тегами:
<!-- BaseCard.vue -->
<template>
<div class="card">
<slot></slot> <!-- сюда попадёт содержимое -->
</div>
</template>
<!-- App.vue -->
<BaseCard>
<h3>Заголовок</h3>
<p>Любая разметка внутри карточки</p>
</BaseCard>
Именованные слоты
Когда мест для вставки несколько (шапка, тело, подвал), их различают именами. Компонент задаёт <slot name="header">, а родитель целит в него через <template #header>:
<!-- BaseCard.vue -->
<template>
<div class="card">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
</div>
</template>
<!-- App.vue -->
<BaseCard>
<template #header><h3>Заголовок</h3></template>
<p>Это попадёт в слот по умолчанию</p>
</BaseCard>
Пропсы, события, слоты — кто за что
| Механизм | Направление | Передаёт |
| Пропсы | родитель → ребёнок | данные |
| События | ребёнок → родитель | сигналы и значения |
| Слоты | родитель → ребёнок | разметку |
Итог
- Ребёнок объявляет события через
defineEmitsи испускает их черезemit('имя', данные). - Родитель слушает событие через
@имя="обработчик". - Слоты передают разметку внутрь компонента;
<slot>задаёт место вставки. - Именованные слоты (
name+#имя) позволяют несколько точек вставки.