Олег Марков
Компоненты Vue создание передача данных события и emit
Введение
Vue — один из самых популярных фреймворков для построения пользовательских интерфейсов, и ключевая возможность Vue — это компоненты. Компоненты позволяют дробить интерфейс на независимые, переиспользуемые блоки, которыми легко управлять и комбинировать. В этой статье вы узнаете, как создавать компоненты во Vue, передавать в них данные, реагировать на события и отправлять данные обратно во внешние компоненты с помощью механизма emit. Я проведу вас через базовую теорию и покажу всё на примерах, чтобы после прочтения вы могли уверенно использовать компоненты в своих проектах.
Что такое компоненты во Vue
Компоненты Vue — это переиспользуемые части интерфейса со своим шаблоном, логикой и стилями. Каждый компонент изолирован: он может содержать в себе другие компоненты, получать данные от родителя и отправлять события наружу.
Вот главные преимущества работы с компонентами:
- Повторное использование кода — один и тот же компонент можно использовать в разных местах.
- Легче поддерживать и развивать большие приложения.
- Ясное разделение ответственности между частями интерфейса.
Давайте сразу посмотрим, как создать свой первый компонент.
Создание компонента во Vue
Есть несколько способов определить компонент. Я начну с самого простого — глобальная и локальная регистрация.
Глобальная регистрация
Глобально зарегистрированный компонент доступен во всём приложении:
// main.js
import Vue from 'vue'
Vue.component('MyButton', {
template: '<button>Нажми меня</button>'
})
Теперь MyButton
можно использовать в любом шаблоне вашего приложения.
Локальная регистрация
Локальная регистрация позволяет использовать компонент только внутри текущего родительского компонента:
// MyButton.vue
<template>
<button>Нажми меня</button>
</template>
<script>
export default {
name: 'MyButton'
}
</script>
// ParentComponent.vue
<template>
<div>
<MyButton />
</div>
</template>
<script>
import MyButton from './MyButton.vue'
export default {
components: { MyButton }
}
</script>
Здесь MyButton
будет доступен только внутри ParentComponent
.
Структура компонента
Компонент обычно состоит из трех частей:
- template — разметка компонента
- script — логика (определение данных, методов, props и т.д.)
- style — стили (опционально)
Вот базовый пример:
<template>
<div>
<h2>{{ title }}</h2>
</div>
</template>
<script>
export default {
props: ['title'] // Ожидаем получение 'title' от родителя
}
</script>
<style scoped>
/* Стили только для этого компонента */
h2 {
color: #42b983;
}
</style>
Передача данных между компонентами
В Vue данные можно передавать между компонентами разными способами:
- От родителя к потомку через props.
- От потомка к родителю через события (и emit).
- Коммуникация между одноуровневыми или далекими компонентами с помощью внешнего хранилища (например, Vuex) или provide/inject (этот способ менее базовый, о нём кратко расскажу ближе к концу).
Передача данных вниз: props
Props — это способ передать данные от родителя к дочернему компоненту.
Давайте посмотрим, как это работает.
Пример
<!-- ParentComponent.vue -->
<template>
<CustomMessage text="Привет из родителя!" />
</template>
<script>
import CustomMessage from './CustomMessage.vue'
export default {
components: { CustomMessage }
}
</script>
<!-- CustomMessage.vue -->
<template>
<div>
<p>{{ text }}</p>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
required: true // Проверяет, что prop передан и он строка
}
}
}
</script>
Здесь строка text
передается как prop из родителя дочке. Vue умеет валидировать типы пропсов, требовать их обязательное наличие, указывать значения по умолчанию.
Проверка и настройка props
Вы можете указывать тип пропса, флаг обязательности и значение по умолчанию:
props: {
color: {
type: String, // Только строка
default: 'black', // Значение по умолчанию
required: false // Не обязателен
}
}
Механизм односторонней передачи
Важно помнить: пропсы односторонние. Дочерний компонент не должен менять свой prop напрямую. Если пробовать, Vue выдаст предупреждение. Если нужно изменить данные — эмитим событие и обновляем их в родителе (этот момент рассмотрим ниже).
Передача данных вверх: события и emit
Если нужно отправить данные от дочернего компонента к родителю, используется механизм событий с помощью метода $emit
.
Базовый пример
<!-- CustomButton.vue -->
<template>
<button @click="handleClick">Кликнуть</button>
</template>
<script>
export default {
methods: {
handleClick() {
// Отправляем событие 'custom-click'
this.$emit('custom-click')
}
}
}
</script>
<!-- ParentComponent.vue -->
<template>
<CustomButton @custom-click="doSomething" />
</template>
<script>
import CustomButton from './CustomButton.vue'
export default {
components: { CustomButton },
methods: {
doSomething() {
// Этот метод вызовется, когда CustomButton сэмитит событие
// Можно здесь обновить родительские данные
alert('Событие от потомка!')
}
}
}
</script>
Передача параметров вместе с событием
$emit
может передавать значения. Это очень часто используется для передачи новых значений или каких-то данных.
<!-- CustomInput.vue -->
<template>
<input :value="value" @input="onInput" />
</template>
<script>
export default {
props: ['value'],
methods: {
onInput(event) {
// Передаете новое значение ввода родителю
this.$emit('input', event.target.value)
}
}
}
</script>
<!-- ParentComponent.vue -->
<template>
<CustomInput v-model="myData" />
</template>
<script>
import CustomInput from './CustomInput.vue'
export default {
components: { CustomInput },
data() {
return {
myData: ''
}
}
}
</script>
Здесь используется v-model
— это синтаксический сахар для события input
и prop value
. Благодаря emit и props компонент ведёт себя как стандартный input!
Детали работы $emit
$emit
можно использовать в любом методе компонента.- Имя события — строка (Vue сам приводит к kebab-case).
- Аргументы, переданные в emit, потом доступны в обработчике события в родителе.
Как слушать пользовательские события
Событие, сэмитенное компонентом, можно слушать на том же уровне, где используется этот компонент:
<ChildComp @my-event="onChildEvent" />
Вложенные компоненты и проксирование событий
Если у вас несколько уровней компонентов и нужно передать событие через промежуточный компонент, можно использовать проксирование emit:
// Intermediate.vue
<template>
<Child @something="forward" />
</template>
<script>
export default {
methods: {
forward(payload) {
this.$emit('something', payload) // Переправляет наверх
}
}
}
</script>
Это полезно, если ваш компонент — просто обёртка, и он проксирует внутренние события дальше наверх.
Слоты: ещё один способ передачи данных
Слоты позволяют вкладывать элементы между тегами компонента при его использовании:
<!-- MyModal.vue -->
<template>
<div class="modal">
<slot /> <!-- Здесь будут дочерние элементы -->
</div>
</template>
<!-- ParentComponent.vue -->
<MyModal>
<h2>Это заголовок модального окна</h2>
<p>Детали модального окна</p>
</MyModal>
С помощью слотов можно делать компоненты еще более универсальными, комбинируя логику и разметку.
Scoped slots (Слоты с областью видимости)
В некоторых случаях компонент предоставляет данные для слота:
<!-- ListRenderer.vue -->
<template>
<div>
<slot v-for="item in items" :item="item" :key="item.id" />
</div>
</template>
<script>
export default {
props: ['items']
}
</script>
<!-- Родительский компонент -->
<ListRenderer :items="myItems" v-slot="{ item }">
<div>
{{ item.name }}
</div>
</ListRenderer>
Теперь слот получает item
прямо из дочернего компонента.
Reactivity: реактивные данные и обновления
Vue использует реактивность — когда вы изменяете данные в родителе, значения, переданные через props, автоматически обновляются в потомке. Если ребенок меняет данные через emit, родитель тоже мгновенно получает изменения.
Этот механизм лежит в основе взаимодействия всех компонентов.
Провайдер и инжект (provide/inject)
Иногда нужно передать данные сразу множеству вложенных компонентов, минуя промежуточных. Тогда вы можете использовать подход provide/inject:
// Родитель
provide() {
return {
themeColor: 'green'
}
}
// Далёкий потомок
inject: ['themeColor']
Этот способ используется в продвинутых случаях, когда props и emit уже становятся неудобными.
Пример большого приложения с компонентами
Давайте разберём простой CRUD-интерфейс: список задач с добавлением и удалением.
TaskItem.vue
<template>
<li>
{{ task.text }}
<button @click="remove">Удалить</button>
</li>
</template>
<script>
export default {
props: ['task'],
methods: {
remove() {
// Сообщаем родителю, что задачу надо удалить
this.$emit('remove-task', this.task.id)
}
}
}
</script>
TaskList.vue
<template>
<ul>
<TaskItem
v-for="task in tasks"
:key="task.id"
:task="task"
@remove-task="onRemove"
/>
</ul>
</template>
<script>
import TaskItem from './TaskItem.vue'
export default {
props: ['tasks'],
components: { TaskItem },
methods: {
onRemove(id) {
// Передает событие ещё выше
this.$emit('remove-task', id)
}
}
}
</script>
App.vue (родитель)
<template>
<div>
<input v-model="newTask" @keyup.enter="addTask" placeholder="Новая задача" />
<TaskList
:tasks="tasks"
@remove-task="removeTask"
/>
</div>
</template>
<script>
import TaskList from './TaskList.vue'
export default {
components: { TaskList },
data() {
return {
tasks: [],
newTask: ''
}
},
methods: {
addTask() {
if (!this.newTask.trim()) return
this.tasks.push({
id: Date.now(),
text: this.newTask
})
this.newTask = ''
},
removeTask(id) {
this.tasks = this.tasks.filter(task => task.id !== id)
}
}
}
</script>
Видите, как данные "спускаются" через props и "поднимаются" через события и emit? Именно так строится большинство приложений на Vue.
Итоги
Компоненты — это основа любого проекта на Vue. Вы узнали, как и зачем их создавать, как передавать данные вниз (props) и вверх (emit), как слушать события, использовать слоты и реализовывать вложенную коммуникацию. Начните с маленьких компонентов, объединяйте их и выстроите свой интерфейс по частям — это очень удобно и эффективно.
Частозадаваемые технические вопросы по теме
Как передать функцию из родителя в дочерний компонент и вызвать её там?
Передайте функцию через prop, а в дочке вызовите её как любой другой метод:
// Родитель
<MyComponent :myFunc="handleFunc" />
// Дочерний компонент
props: ['myFunc']
...
this.myFunc() // вызывает переданную функцию
Можно ли синхронизировать данные между дочерними компонентами напрямую?
Нет, рекомендовано использовать только emit и props или общее хранилище (Vuex/Pinia). Или прокинуть данные через provide/inject, если компоненты иерархически связаны.
Как отслеживать все события, исходящие от потомка?
Добавьте для компонента v-on="$listeners"
(Vue 2) или v-bind="$attrs"
(Vue 3) — так можно проксировать все события/props.
Как валидация props помогает избежать ошибок?
Объявляйте тип, обязательность и значения по умолчанию. Vue уведомит вас, если проп не передан или передан неверного типа. Это уменьшает "таинственные" баги.
Как обновить prop из родителя, если его значение (например, число для счетчика) изменяется внутри потомка?
Ребёнок эмитит событие с новым значением. Родитель ловит событие, меняет своё значение, а оно автоматически "спускается" в пропс обратно в дочерний компонент. Именно так работает v-model
. Никогда не изменяйте проп напрямую!