Vue: передача данных между компонентами
или отправка данных с помощью событий
Обновлено 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);
- https://ru.vuejs.org/v2/guide/components.html#%D0%9F%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B8%D0%B5-%D1%81%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D1%8F
- https://ru.vuejs.org/v2/guide/components.html#%D0%9A%D0%BE%D0%BC%D0%BC%D1%83%D0%BD%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F-%D0%BC%D0%B5%D0%B6%D0%B4%D1%83-%D0%BA%D0%BE%D0%BC%D0%BF%D0%BE%D0%BD%D0%B5%D0%BD%D1%82%D0%B0%D0%BC%D0%B8-%D0%BD%D0%B5-%D1%81%D0%B2%D1%8F%D0%B7%D0%B0%D0%BD%D0%BD%D1%8B%D0%BC%D0%B8-%D0%B8%D0%B5%D1%80%D0%B0%D1%80%D1%85%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8