Vue: передача данных между компонентами

или отправка данных с помощью событий

Alex Sokolov
4 min readNov 12, 2016

Обновлено 06.01.2018: Пересмотрел свежим взглядом и внёс мелкие правки. Заходите и в Telegram-канал по Vue: https://t.me/vuejs_ru

Компонентный подход — появился не просто так, логика приложения чётко разделяется между его частями, всё изолировано друг от друга, каждая часть знает лишь о том, о чём нужно знать.

Основа работы Vue — однонаправленный поток данных. Это значит что данные из компонентов верхних уровней, передаются в компоненты нижних уровней через входные параметры (или props). А для обратной связи наверх используются события (дочерние компоненты уведомляют о произошедшем событии и, возможно, передают какие-то данные).

В качестве примера возьмём два компонента: список и элемент списка. Каждый в своём vue-файле, глобально их не регистрируем — родительский узнаёт о дочернем из своей секции components.

В родительском расположена логика работы со списком: как загружать элементы, как добавлять новый элемент в список и как удалять.

Дочерний занимается только отображением самого элемент списка. Кроме того, в дочернем элементе списка есть кнопка удаления. Нужно по нажатию кнопки удалять этот элемент из списка.

Сам элемент себя не удалит, он даже не знает что такой список есть и что он в нём находится. Поэтому дочернему компоненту надо как-то родительский компонент уведомить о необходимости удаления и заодно передать id для ясности.

Есть несколько вариантов, начнём с правильного…

Генерация события в дочернем компоненте и обработка в родительском

Официальная документация предлагает пользоваться событиями: один компонент их генерирует, другой слушает и уже сам как-то занимается обработкой. Для генерации и отслеживания событий есть два служебных метода $emit и $on:

// Генерируем событие, возможно с передаваемыми данными
this.$emit('название' [, данные]);
// Слушаем событие
this.$on('название', function(данные) {...});

В дочернем компоненте делаем обработчик нажатия кнопки удаления и в методе обработчика генерируем событие:

// в <template>
<button type="button" @click="removeItem">Удалить</button>
// в <script>
methods: {
removeItem: function() {
// генерируем событие 'remove' и передаём id элемента
this.$emit('remove', this.item.id);
}
}

Если в дочерний компонент передаётся элемент item с свойством ID, то можем сразу отправлять наверх нужный ID для удаления. Но можно и просто генерировать событие, а ID подставлять в обработчик в родительском компоненте (см. пример ниже).

Также не стоит забывать что это JS и не собирать грабли именуя подобные методы delete. Использование зарезервированных слов может закончиться часами увлекательной отладки.

В родительском компоненте на тег компонента в шаблоне нужно добавить директиву v-on:название-события с указанием метода обработчика для этого события:

<list-item v-bind:item="item" v-on:remove="removeFromList" />// ... или сокращённый вариант записи
<list-item :item="item" @remove="removeFromList" />
// ... если не передавали снизу с событием ID элемента
<list-item :item="item" @remove="removeFromList(item.id)" />

Вместо создания непонятных методов обработчиков onRemove или onSubmit, лучше присваивать имена более приближенные к выполняемой им логике.

Вот и всё! Теперь при генерации события remove в дочернем компоненте, родительский также отследит его и выполнит свой собственный метод removeFromList, любезно передав в него id.

Так как родительский компонент оперирует всем списком — ему и удалять элемент. Например, можно реализовать через .filter, а стрелочная функция сделает код ещё лаконичнее и компактнее:

// (!) используются ES6 возможности языкаmethods: {
removeFromList(id) {
this.list = this.list.filter(item => item.id !== id)
}
}

Задача реализована, связности между компонентами нет, всё прекрасно работает. Нет никаких проблем с дальнейшним переиспользованием компонента для элемента списка в другом месте.

Использование шины событий

Есть ещё один вариант передачи данных между компонентами — шина. Причем не только из дочернего компонента в родительский, а между любыми, сколь угодно разрозненными, частями приложения.

При использовании модулей мы создаём пустой экземпляр Vue и экспортируем его — он и будет служить нашей шиной событий. Если мы не используем модули, то переменную bus необходимо сделать глобальной или каким-то другим способом сделать доступной для обращения в других местах:

// bus.js (создаем общую шину и экспортируем)
export const bus = new Vue()

В файлах, которые будут использовать эту шину:

// a.vue (импортируем шину и генерируем в неё события)
import { bus } from 'bus.js'
bus.$emit('mega-event'[, данные]);// b.vue (импортируем шину и отслеживаем в ней события)
import { bus } from 'bus.js'
bus.$on('mega-event', this.megaEventHandler)

Если плодить глобальные переменные не хочется совсем, а все необходимые части находятся внутри одного экземпляра Vue, то в качестве шины можно использовать корневой экземпляр:

// Генерируем событие на глобальном уровне
this.$root.$emit('remove', id);

В других компонентах в секцию mounted добавляем прослушивание:

this.$root.$on('remove', function (id) {...})

Варианты дальше имеют место быть в исключительных случаях, когда вы полностью отдаёте себе понимание в необходимости этого! Их использование отразится на ясности вашего кода (надо будет не забывать про наличие этих “костылей”) и добавит связности между компонентами, что усложнит переиспользование в других частях вашего приложения.

Вызов родительского метода из дочернего компонента

В дочернем компоненте мы можем через служебную переменную $parent “выбраться” на уровень родительского компонента и вызывать его методы:

// this.$parent для доступа к родительскому компоненту
this.$parent.$options.methods.removeFromList(id);
// или this.$root для доступа к корневому компоненту
this.$root.$options.methods.removeFromList(id);

Чем плох этот вариант? Компоненты становятся явно зависимы друг от друга — изменения родительского могут потребовать правок в дочернем, или используя дочерний в другом месте приложения потребуется реализовывать точно такой же метод в другом родительском компоненте.

Генерация события в родительском компоненте из дочернего

Основываясь на предыдущем способе можно “вылезать наверх” из дочернего в родительский и уже там генерировать событие:

this.$parent.$emit('remove', id);

В родительском компоненте, например в секции mounted, можно отслеживать события нужного типа:

this.$on('remove', function (id) {...})// ... или даже сразу вызывать нужный метод
this.$on('remove', this.removeFromList);

--

--