← Курс/Emits: коммуникация child → parent#221 из 257+30 XP

Emits: коммуникация child → parent

Зачем нужны emits

Props текут от родителя к ребёнку. Но что если ребёнок должен сообщить родителю о действии пользователя — клике кнопки, отправке формы, изменении значения? Для этого используются **события (emits)**.

defineEmits

В Vue 3 Composition API события объявляются через defineEmits:

// ChildForm.vue
import { defineEmits } from 'vue'

const emit = defineEmits(['submit', 'cancel', 'update:modelValue'])

function handleSubmit() {
  emit('submit', { name: 'Alice', age: 30 })
}

function handleCancel() {
  emit('cancel')
}

Объявление с валидацией

const emit = defineEmits({
  // null — без валидации
  cancel: null,

  // функция-валидатор payload
  submit: (payload) => {
    if (!payload.name) {
      console.warn('submit: name is required')
      return false
    }
    return true
  }
})

Слушаем события в родителе

<!-- Parent.vue -->
<template>
  <ChildForm
    @submit="handleFormSubmit"
    @cancel="showForm = false"
  />
</template>

<script setup>
function handleFormSubmit(payload) {
  console.log('Получили:', payload.name)
}
</script>

v-model с компонентами

v-model — это синтаксический сахар над props + emit. По умолчанию:

  • prop: modelValue
  • emit: update:modelValue
  • // CustomInput.vue
    const props = defineProps(['modelValue'])
    const emit = defineEmits(['update:modelValue'])
    
    function onInput(e) {
      emit('update:modelValue', e.target.value)
    }
    <!-- Родитель: -->
    <CustomInput v-model="searchQuery" />
    <!-- эквивалентно: -->
    <CustomInput :modelValue="searchQuery" @update:modelValue="searchQuery = $event" />

    Именованный v-model (Vue 3.4+)

    <!-- Несколько v-model на одном компоненте -->
    <UserForm v-model:name="userName" v-model:email="userEmail" />

    Паттерн child → parent коммуникации

    Родитель                      Ребёнок
       |                              |
       |------- props (данные) ------>|
       |                              |
       |<------ emit (события) -------|
       |                              |

    Ребёнок никогда не изменяет props напрямую. Вместо этого он сообщает родителю через событие, и родитель сам решает — обновлять данные или нет.

    Примеры

    Паттерн Publisher/Subscriber для компонентной коммуникации

    // EventEmitter — аналог системы emit Vue
    class EventEmitter {
      constructor() {
        this._listeners = {}
      }
    
      on(event, callback) {
        if (!this._listeners[event]) {
          this._listeners[event] = []
        }
        this._listeners[event].push(callback)
        // Возвращаем функцию отписки
        return () => this.off(event, callback)
      }
    
      off(event, callback) {
        if (!this._listeners[event]) return
        this._listeners[event] = this._listeners[event].filter(cb => cb !== callback)
      }
    
      emit(event, ...args) {
        const listeners = this._listeners[event] || []
        listeners.forEach(cb => cb(...args))
      }
    }
    
    // Дочерний компонент — форма
    function createChildForm(emitter) {
      return {
        // Имитируем действие пользователя
        submit(value) {
          console.log(`[ChildForm] отправляем: ${value}`)
          emitter.emit('submit', { value, timestamp: Date.now() })
        },
        cancel() {
          console.log('[ChildForm] отменяем')
          emitter.emit('cancel')
        }
      }
    }
    
    // Родительский компонент
    function createParent() {
      const emitter = new EventEmitter()
      const child = createChildForm(emitter)
    
      // Подписываемся на события ребёнка — аналог @submit в шаблоне
      emitter.on('submit', (payload) => {
        console.log(`[Parent] получили submit: "${payload.value}"`)
      })
    
      emitter.on('cancel', () => {
        console.log('[Parent] форма отменена')
      })
    
      return { child }
    }
    
    const parent = createParent()
    
    // Имитируем действия пользователя
    parent.child.submit('Hello, Vue!')
    // [ChildForm] отправляем: Hello, Vue!
    // [Parent] получили submit: "Hello, Vue!"
    
    parent.child.cancel()
    // [ChildForm] отменяем
    // [Parent] форма отменена
    
    // v-model паттерн: двустороннее связывание через emit
    function createModelInput(emitter, initialValue) {
      let modelValue = initialValue
    
      // Аналог @update:modelValue
      emitter.on('update:modelValue', (newValue) => {
        modelValue = newValue
        console.log(`[Parent] modelValue обновлён: "${newValue}"`)
      })
    
      return {
        // Имитируем ввод пользователя
        input(value) {
          emitter.emit('update:modelValue', value)
        },
        getValue: () => modelValue,
      }
    }
    
    const modelEmitter = new EventEmitter()
    const inputComp = createModelInput(modelEmitter, '')
    inputComp.input('Alice')  // [Parent] modelValue обновлён: "Alice"
    inputComp.input('Bob')    // [Parent] modelValue обновлён: "Bob"