Kurs Vue 3: zdarzenia z potomka do rodzica (defineEmits)
zweryfikowano 2 września 2026 · 3 min
Komponent potomny sygnalizuje rodzicowi zmianę, emitując zdarzenie: defineEmits je deklaruje, emit('add-to-cart', id) je wysyła, a rodzic nasłuchuje przez @add-to-cart. Propsy schodzą w dół, zdarzenia idą w górę.
Propsy schodzą od rodzica do potomka. Żeby przekazać informację w górę, komponent emituje zdarzenie, a rodzic go nasłuchuje, dokładnie tak, jak nasłuchiwałby kliknięcia.
Problem
Od podziału z rozdziału 8 koszyk mieszka w App.vue, a przycisk w ProductDisplay.vue. Potomek nie może zmienić cart: nie ma do niego dostępu, a komponent nie powinien pisać po stanie innego komponentu.
Właściwe sformułowanie to nie „potomek dodaje do koszyka”, tylko „potomek ogłasza, że ktoś kliknął, a rodzic decyduje, co z tym zrobić”.
Zadeklarować i wyemitować
W potomku zadeklaruj emitowane zdarzenia, a potem emituj:
<script setup>
const emit = defineEmits(['add-to-cart'])
function addToCart() {
emit('add-to-cart')
}
</script>
<template>
<button
class="button"
:class="{ disabledButton: !inStock }"
:disabled="!inStock"
@click="addToCart"
>
Ajouter au panier
</button>
</template>defineEmits to makro kompilatora, tak samo jak defineProps: bez importu. Zwraca funkcję, która służy do emitowania.
Deklarowanie zdarzeń nie jest obowiązkowe, ale się przydaje: lista dokumentuje kontrakt komponentu, edytor podpowiada ją automatycznie, a Vue ostrzega, kiedy wyemitujesz niezadeklarowaną nazwę.
Nasłuch po stronie rodzica
Własnego zdarzenia nasłuchuje się tą samą składnią co zdarzenia natywnego:
<script setup>
import { ref } from 'vue'
import ProductDisplay from './components/ProductDisplay.vue'
const cart = ref(0)
function updateCart() {
cart.value += 1
}
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay :premium="premium" @add-to-cart="updateCart" />
</template>Kliknij: licznik znów rusza. Przycisk jest w potomku, licznik w rodzicu i żaden z nich nie zna stanu drugiego.
Przekazanie danych
Realistyczny koszyk pamięta, co się do niego wkłada, a nie tylko ile. Przekaż identyfikator wariantu jako drugi argument emit:
<script setup>
function addToCart() {
emit('add-to-cart', variants.value[selectedVariant.value].id)
}
</script>Rodzic dostaje go jako parametr swojej funkcji:
<script setup>
const cart = ref([])
function updateCart(id) {
cart.value.push(id)
}
</script>
<template>
<div class="cart">Panier ({{ cart.length }})</div>
<ProductDisplay :premium="premium" @add-to-cart="updateCart" />
</template>cart stał się tablicą, więc wyświetlanie korzysta z cart.length. Dodaj kilka razy niebieski T-shirt, a potem sprawdź w narzędziach deweloperskich Vue, że tablica faktycznie zapełnia się wartościami 2234.
Przy kilku informacjach emituj obiekt zamiast długiej listy argumentów:
emit('add-to-cart', { id: variant.id, taille: 'M', quantite: 1 })Nazwy zdarzeń
Nazwę pisz w kebab-case: add-to-cart, nie addToCart. Atrybuty HTML nie rozróżniają wielkości liter, szablon napisany wprost w stronie dostałby @addtocart i nasłuch nigdy by się nie odpalił. W pliku .vue Vue nadrabia wielkość liter, ale lepiej od razu wyrobić sobie dobry nawyk.
Własne zdarzenie propaguje się tylko do bezpośredniego rodzica. Jeśli nadawcę od odbiorcy dzielą trzy poziomy komponentów, trzeba je reemitować na każdym piętrze. Kiedy staje się to męczące, to sygnał, że potrzebujesz współdzielonego stanu: dokładnie do tego służy Pinia.
Kierunek obiegu
Opinię, która przychodzi tą drogą, można potem animować przy wyświetlaniu.
Zapamiętaj dwa zdania: propsy schodzą w dół, zdarzenia idą w górę. Rodzic jest właścicielem danych, potomek prosi o ich zmianę. To właśnie czyni przepływ przewidywalnym, a źródło każdej zmiany zawsze możliwym do prześledzenia.
Zdarzenia deklaruje się w opcji emits, a emituje przez this.$emit('add-to-cart', id). Rodzic nasłuchuje ich dokładnie tak samo.
remove-from-cart emitowane przez drugi przycisk i spraw, żeby rodzic usuwał z tablicy ostatni identyfikator.Częste błędy
@addtocart i nasłuch nigdy by się nie odpalił. Używaj kebab-case.