Связь между родственными компонентами в VueJs 2.0

113

Обзор

В Vue.js 2.x model.syncбудет объявлен устаревшим .

Итак, каков правильный способ взаимодействия между соседними компонентами в Vue.js 2.x ?


Задний план

Насколько я понимаю, Vue 2.x, предпочтительный метод для связи между братьями и сестрами - использовать хранилище или шину событий .

По словам Эвана (создателя Vue):

Также стоит упомянуть, что «передача данных между компонентами» - это вообще плохая идея, потому что в конце концов поток данных становится неотслеживаемым и очень трудным для отладки.

Если часть данных должна совместно использоваться несколькими компонентами, предпочтите глобальные хранилища или Vuex .

[ Ссылка на обсуждение ]

И:

.onceи .syncустарели. Опоры теперь всегда в одну сторону. Чтобы вызвать побочные эффекты в родительской области, компоненту необходимо явно emitявить событие, а не полагаться на неявную привязку.

Итак, Эван предлагает использовать $emit()и $on().


Проблемы

Что меня беспокоит:

  • У каждого storeи eventесть глобальная видимость (поправьте меня, если я ошибаюсь);
  • Слишком расточительно создавать новый магазин для каждого незначительного сообщения;

То , что я хочу , это какое - то рамка events или storesвидимость для сибсов компонентов. (Или, возможно, я не понял вышеупомянутую идею.)


Вопрос

Итак, каков правильный способ связи между одноуровневыми компонентами?

Сергей Панфилов
источник
2
$emitв сочетании с v-modelподражать .sync. Я думаю, вам стоит пойти по пути
Vuex
3
Так что я подумал о том же беспокойстве. Мое решение - использовать эмиттер событий с широковещательным каналом, который эквивалентен 'scope', то есть дочерняя / родительская и родственная установка используют один и тот же канал для связи. В моем случае я использую радио-библиотеку radio.uxder.com, потому что это всего лишь несколько строк кода и его пуленепробиваемость, но многие выбрали бы узел EventEmitter.
Tremendus Apps

Ответы:

84

В Vue 2.0 я использую механизм eventHub, как показано в документации .

  1. Определите централизованный центр событий.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Теперь в вашем компоненте вы можете генерировать события с помощью

    this.eventHub.$emit('update', data)
  3. И слушать, как ты

    this.eventHub.$on('update', data => {
    // do your thing
    })

Обновление См. Ответ @alex , в котором описывается более простое решение.

какони
источник
3
Внимание : следите за глобальными миксинами и старайтесь по возможности избегать их, поскольку, согласно этой ссылке vuejs.org/v2/guide/mixins.html#Global-Mixin, они могут повлиять даже на сторонние компоненты.
Vini.g. от
6
Гораздо более простое решение - использовать то, что описал @Alex - this.$root.$emit()иthis.$root.$on()
Webnet
5
Для справки в будущем не обновляйте свой ответ чужим ответом (даже если вы думаете, что это лучше, и ссылаетесь на него). Ссылка на альтернативный ответ или даже попросите OP принять другой ответ, если вы считаете, что он должен, но копирование их ответа в свой собственный является плохой формой и отбивает у пользователей желание отдавать должное там, где это необходимо, поскольку они могут просто проголосовать только за ваш только ответ. Поощряйте их перейти к ответу, на который вы ссылаетесь, (и, таким образом, проголосовать за него), не включая этот ответ в свой собственный.
GrayedFox
4
Спасибо за ценный отзыв @GrayedFox, соответствующим образом обновил свой ответ.
kakoni
2
Пожалуйста , обратите внимание , что это решение больше не будет поддерживаться в Vue 3. См stackoverflow.com/a/60895076/752916
AlexMA
148

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

Компонент 1:

this.$root.$emit('eventing', data);

Компонент 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Alex
источник
2
Это работает лучше, чем определение дополнительного концентратора событий и присоединение его к любому потребителю событий.
schad
2
Я большой поклонник этого решения, так как мне очень не нравятся события, имеющие масштаб. Однако я не работаю с VueJS каждый день, поэтому мне любопытно, есть ли кто-нибудь, кто видит проблемы с этим подходом.
Webnet
2
Самое простое решение из всех ответов
Викаш Гупта
1
красиво, коротко и легко реализовать, а также легко понять
нада
1
Если вы хотите исключительно прямое
общение между
47

Типы общения

При разработке приложения Vue (или, фактически, любого приложения на основе компонентов) существуют разные типы связи, которые зависят от того, с какими проблемами мы имеем дело, и у них есть свои собственные каналы связи.

Бизнес-логика: относится ко всему, что связано с вашим приложением и его целью.

Логика представления: все, с чем взаимодействует пользователь или что является результатом взаимодействия с пользователем.

Эти две проблемы связаны с этими типами общения:

  • Состояние приложения
  • Родитель-ребенок
  • Ребенок-родитель
  • Братья и сестры

Каждый тип должен использовать правильный канал связи.


Каналы связи

Канал - это общий термин, который я буду использовать для обозначения конкретных реализаций для обмена данными в приложении Vue.

Реквизиты: логика представления родитель-ребенок

Самый простой канал связи в Vue для прямого общения родителей и детей . В основном его следует использовать для передачи данных, относящихся к логике представления или ограниченному набору данных вниз по иерархии.

Ссылки и методы: Презентационный антипаттерн

Когда нет смысла использовать опору, позволяющую дочернему элементу обрабатывать событие от родителя, настройка refдочернего компонента и вызов его методов - это нормально.

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

События: логика представления дочернего и родительского элементов

$emitи $on. Самый простой канал связи для прямого общения Родитель-Ребенок. Опять же, следует использовать для логики представления.

Автобус событий

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

Это может стать полезным при передаче props повсюду, начиная с самых глубоко вложенных дочерних компонентов, и почти никаким другим компонентам они не нужны. Используйте осторожно для тщательно отобранных данных.

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

Ниже показано, как простая ошибка приводит к утечке, когда Itemкомпонент все еще запускается, даже если он удален из DOM.

Не забудьте удалить слушателей в destroyedловушке жизненного цикла.

Централизованный магазин (Бизнес-логика)

Vuex - это способ использовать Vue для управления состоянием . Он предлагает гораздо больше, чем просто события, и готов к полномасштабному применению.

А теперь вы спрашиваете :

[S] Могу ли я создать магазин vuex для каждого второстепенного сообщения?

Он действительно сияет, когда:

  • работа с вашей бизнес-логикой,
  • связь с серверной частью (или любым уровнем сохранения данных, например локальным хранилищем)

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

Это не означает, что вы не можете использовать его для логики компонентов, но я бы применил эту логику к модулю Vuex с пространством имен только с необходимым глобальным состоянием пользовательского интерфейса.

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


Типы компонентов

Чтобы организовать все эти коммуникации и упростить повторное использование, мы должны рассматривать компоненты как два разных типа.

  • Контейнеры для конкретных приложений
  • Общие компоненты

Опять же, это не означает, что общий компонент следует повторно использовать или что конкретный контейнер приложения нельзя повторно использовать, но у них разные обязанности.

Контейнеры для конкретных приложений

Это простой компонент Vue, который обертывает другие компоненты Vue (общие или другие контейнеры, специфичные для приложения). Здесь должно происходить взаимодействие хранилища Vuex, и этот контейнер должен взаимодействовать с помощью других более простых средств, таких как реквизиты и прослушиватели событий.

Эти контейнеры могут вообще не иметь собственных элементов DOM и позволять универсальным компонентам обрабатывать шаблоны и взаимодействие с пользователем.

Объем каким - то образом eventsили storesвидимость для сибсов компонентов

Вот где происходит определение объема работ. Большинство компонентов не знают о хранилище, и этот компонент должен (в основном) использовать один модуль хранилища с пространством имен с ограниченным набором gettersи actionsприменяться с предоставленными помощниками привязки Vuex .

Общие компоненты

Они должны получать свои данные от props, вносить изменения в свои локальные данные и генерировать простые события. В большинстве случаев они не должны знать, что магазин Vuex вообще существует.

Их также можно назвать контейнерами, поскольку их единственная ответственность может заключаться в отправке другим компонентам пользовательского интерфейса.


Братское общение

Итак, после всего этого, как мы должны общаться между двумя родственными компонентами?

Это легче понять на примере: допустим, у нас есть поле ввода, и его данные должны совместно использоваться в приложении (братья и сестры в разных местах в дереве) и сохраняться с помощью серверной части.

Начиная с наихудшего сценария , наш компонент будет смешивать презентацию и бизнес- логику.

// MyInput.vue
<template>
    <div class="my-input">
        <label>Data</label>
        <input type="text"
            :value="value" 
            :input="onChange($event.target.value)">
    </div>
</template>
<script>
    import axios from 'axios';

    export default {
        data() {
            return {
                value: "",
            };
        },
        mounted() {
            this.$root.$on('sync', data => {
                this.value = data.myServerValue;
            });
        },
        methods: {
            onChange(value) {
                this.value = value;
                axios.post('http://example.com/api/update', {
                        myServerValue: value
                    })
                    .then((response) => {
                        this.$root.$emit('update', response.data);
                    });
            }
        }
    }
</script>

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

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

// MyInput.vue
// the template is the same as above
<script>
    export default {
        props: {
            initial: {
                type: String,
                default: ""
            }
        },
        data() {
            return {
                value: this.initial,
            };
        },
        methods: {
            onChange(value) {
                this.value = value;
                this.$emit('change', value);
            }
        }
    }
</script>

Контейнер, специфичный для нашего приложения, теперь может быть мостом между бизнес-логикой и презентацией.

// MyAppCard.vue
<template>
    <div class="container">
        <card-body>
            <my-input :initial="serverValue" @change="updateState"></my-input>
            <my-input :initial="otherValue" @change="updateState"></my-input>

        </card-body>
        <card-footer>
            <my-button :disabled="!serverValue || !otherValue"
                       @click="saveState"></my-button>
        </card-footer>
    </div>
</template>
<script>
    import { mapGetters, mapActions } from 'vuex';
    import { NS, ACTIONS, GETTERS } from '@/store/modules/api';
    import { MyButton, MyInput } from './components';

    export default {
        components: {
            MyInput,
            MyButton,
        },
        computed: mapGetters(NS, [
            GETTERS.serverValue,
            GETTERS.otherValue,
        ]),
        methods: mapActions(NS, [
            ACTIONS.updateState,
            ACTIONS.updateState,
        ])
    }
</script>

Так как магазин Vuex действия иметь дело с серверной связи, наш контейнер здесь не нужно знать о Вардар и внутреннем интерфейсе.

Эмиль Бержерон
источник
3
Согласитесь с комментарием о том, что методы «
связаны
Мне нравится этот ответ. Но не могли бы вы подробнее рассказать о шине событий и примечании «Будьте осторожны:»? Может быть, вы можете привести какой-нибудь пример, я не понимаю, как компоненты могут быть связаны дважды.
vandroid
Как вы взаимодействуете между родительским компонентом и главным дочерним компонентом, например, при проверке формы. Где родительский компонент - это страница, дочерний - это форма, а главный дочерний элемент - это элемент формы ввода?
Lord Zed
1
@vandroid Я создал простой пример, который показывает утечку, когда слушатели не удаляются должным образом, как и все примеры в этом потоке.
Эмиль Бержерон
@LordZed Это действительно зависит, но, исходя из моего понимания вашей ситуации, это похоже на проблему дизайна. Vue следует использовать в основном для логики представления. Проверка формы должна выполняться в другом месте, например, в интерфейсе стандартного API JS, которое действие Vuex будет вызывать с данными из формы.
Эмиль Бержерон
10

Хорошо, мы можем общаться между братьями и сестрами через родителя, используя v-onсобытия.

Parent
 |-List of items //sibling 1 - "List"
 |-Details of selected item //sibling 2 - "Details"

Предположим, что мы хотим обновить Detailsкомпонент, когда мы щелкаем какой-либо элемент List.


в Parent:

Шаблон:

<list v-model="listModel"
      v-on:select-item="setSelectedItem" 
></list> 
<details v-model="selectedModel"></details>

Вот:

  • v-on:select-itemэто событие, которое будет вызываться в Listкомпоненте (см. ниже);
  • setSelectedItemэто Parentметод обновления selectedModel;

JS:

//...
data () {
  return {
    listModel: ['a', 'b']
    selectedModel: null
  }
},
methods: {
  setSelectedItem (item) {
    this.selectedModel = item //here we change the Detail's model
  },
}
//...

В List:

Шаблон:

<ul>
  <li v-for="i in list" 
      :value="i"
      @click="select(i, $event)">
        <span v-text="i"></span>
  </li>
</ul>

JS:

//...
data () {
  return {
    selected: null
  }
},
props: {
  list: {
    type: Array,
    required: true
  }
},
methods: {
  select (item) {
    this.selected = item
    this.$emit('select-item', item) // here we call the event we waiting for in "Parent"
  },
}
//...

Вот:

  • this.$emit('select-item', item)отправит элемент select-itemнапрямую в родительский. И родитель отправит его в Detailsпредставление
Сергей Панфилов
источник
5

Что я обычно делаю, если хочу «взломать» нормальные шаблоны взаимодействия в Vue, особенно теперь, которые .syncустарели, - это создать простой EventEmitter, который обрабатывает взаимодействие между компонентами. Из одного из моих последних проектов:

import {EventEmitter} from 'events'

var Transmitter = Object.assign({}, EventEmitter.prototype, { /* ... */ })

TransmitterЗатем с помощью этого объекта вы можете в любом компоненте:

import Transmitter from './Transmitter'

var ComponentOne = Vue.extend({
  methods: {
    transmit: Transmitter.emit('update')
  }
})

И чтобы создать «принимающий» компонент:

import Transmitter from './Transmitter'

var ComponentTwo = Vue.extend({
  ready: function () {
    Transmitter.on('update', this.doThingOnUpdate)
  }
})

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

Гектор Лоренцо
источник
1
Я уже использую vuex, но опять же, должен ли я создавать магазин vuex для каждого второстепенного сообщения?
Сергей Панфилов
Мне сложно сказать с таким объемом информации, но я бы сказал, что если вы уже используете vuexда, дерзайте. Используй это.
Hector Lorenzo
1
На самом деле я бы не согласился, что нам нужно использовать vuex для каждого второстепенного сообщения ...
Виктор
Нет, конечно, нет, все зависит от контекста. На самом деле мой ответ уходит от vuex. С другой стороны, я обнаружил, что чем больше вы используете vuex и концепцию объекта центрального состояния, тем меньше я полагаюсь на связь между объектами. Но да, согласен, все зависит от обстоятельств.
Гектор Лоренцо
3

Как вести себя при общении между братьями и сестрами, зависит от ситуации. Но сначала я хочу подчеркнуть, что подход глобальной шины событий уходит в Vue 3 . См. Этот RFC . Поэтому я решил написать новый ответ.

Наименьший образец общего предка (или «LCA»)

В простых случаях я настоятельно рекомендую использовать шаблон «Самый низкий общий предок» (также известный как «данные вниз, события вверх»). Этот шаблон легко читать, внедрять, тестировать и отлаживать.

По сути, это означает, что если два компонента нуждаются в взаимодействии, поместите их общее состояние в ближайший компонент, который является общим предком. Передайте данные от родительского компонента к дочернему компоненту через реквизиты и передайте информацию от дочернего к родительскому, испуская событие (см. Пример этого внизу этого ответа).

В качестве надуманного примера в приложении электронной почты, если компонент «Кому» должен взаимодействовать с компонентом «тело сообщения», состояние этого взаимодействия может находиться в их родительском элементе (возможно, в вызываемом компоненте email-form). У вас может быть опора в email-formвызываемом, addresseeчтобы тело сообщения могло автоматически добавляться Dear {{addressee.name}}к электронной почте на основе адреса электронной почты получателя.

LCA становится обременительной, если коммуникация проходит на большие расстояния с большим количеством посредников. Я часто рекомендую коллегам прочитать этот отличный пост в блоге . (Игнорируйте тот факт, что в его примерах используется Ember; его идеи применимы во многих фреймворках пользовательского интерфейса.)

Шаблон контейнера данных (например, Vuex)

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

Например, было бы разумно создать отдельное пространство имен для сложной коллекции компонентов с множеством взаимосвязей, таких как полнофункциональный компонент календаря.

Шаблон публикации / подписки (шина событий)

Если шаблон шины событий (или «публикация / подписка») более подходит для ваших нужд, основная команда Vue теперь рекомендует использовать стороннюю библиотеку, такую ​​как mitt . (См. RFC, указанный в параграфе 1.)

Бонусные рассылки и код

Вот базовый пример решения Lowest Common Ancestor для общения между братьями и сестрами, проиллюстрированный в игре « Ударь крота» .

Наивный подход может заключаться в том, чтобы думать: «Родинка 1 должна сказать родинке 2, чтобы она появилась после того, как ее ударили». Но Vue не одобряет такой подход, так как он хочет, чтобы мы думали в терминах древовидной структуры .

Наверное, это очень хорошо. Нетривиальное приложение, в котором узлы взаимодействуют напрямую друг с другом через деревья DOM, было бы очень сложно отлаживать без какой-либо системы учета (например, Vuex). Вдобавок ко всему, компоненты, которые используют «данные вниз, события вверх», как правило, демонстрируют низкую взаимосвязь и высокую возможность повторного использования - оба очень желательных качества, которые помогают масштабировать большие приложения.

В этом примере при ударе крота происходит событие. Компонент игрового менеджера решает, какое новое состояние приложения, и, таким образом, родственный крот знает, что делать, неявно после повторного рендеринга Vue. Это несколько банальный пример «низшего общего предка».

Vue.component('whack-a-mole', {
  data() {
    return {
      stateOfMoles: [true, false, false],
      points: 0
    }
  },
  template: `<div>WHACK - A - MOLE!<br/>
    <a-mole :has-mole="stateOfMoles[0]" v-on:moleMashed="moleClicked(0)"/>
    <a-mole :has-mole="stateOfMoles[1]"  v-on:moleMashed="moleClicked(1)"/>
    <a-mole :has-mole="stateOfMoles[2]" v-on:moleMashed="moleClicked(2)"/>
    <p>Score: {{points}}</p>
</div>`,
  methods: {
    moleClicked(n) {
      if(this.stateOfMoles[n]) {
         this.points++;
         this.stateOfMoles[n] = false;
         this.stateOfMoles[Math.floor(Math.random() * 3)] = true;
      }   
    }
  }
})

Vue.component('a-mole', {
  props: ['hasMole'],
  template: `<button @click="$emit('moleMashed')">
      <span class="mole-button" v-if="hasMole">🐿</span><span class="mole-button" v-if="!hasMole">🕳</span>
    </button>`
})

var app = new Vue({
  el: '#app',
  data() {
    return { name: 'Vue' }
  }
})
.mole-button {
  font-size: 2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <whack-a-mole />
</div>

AlexMA
источник