События и слоты

Замыкаем общение компонентов: ребёнок сообщает наверх событиями через 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 + #имя) позволяют несколько точек вставки.
Проверьте себя
1. Как дочерний компонент сообщает родителю о действии пользователя?
AМеняет проп родителя напрямую
BИспускает событие через defineEmits и emit, которое родитель слушает через @
CИмпортирует родителя и вызывает его методы
DЗаписывает данные в localStorage
2. Для чего нужны слоты?
AДля передачи чисел и строк в компонент
BЧтобы родитель мог вставить произвольную разметку внутрь компонента
CДля объявления реактивных переменных
DДля обработки событий мыши
3. Как родитель направляет содержимое в именованный слот header?
A<div slot="header">
B<template #header>...</template>
C<slot name="header">
D:slot="header"
Поддержать проект