VueJs 2.0 испускает событие от великого потомка к его великому родительскому компоненту

89

Похоже, что Vue.js 2.0 не отправляет события от великого потомка к своему основному родительскому компоненту.

Vue.component('parent', {
  template: '<div>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 'No action'
    }
  },
  methods: {
    performAction() { this.action = 'actionDone' }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child></grand-child></div>'
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Do Event</button></div>',
  methods: {
    doEvent() { this.$emit('eventtriggered') }
  }
})

new Vue({
  el: '#app'
})

Этот JsFiddle решает проблему https://jsfiddle.net/y5dvkqbd/4/ , но с помощью двух событий:

  • Один от великого ребенка до среднего компонента
  • Затем снова испускаем от среднего компонента к основному родителю

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

BassMHL
источник

Ответы:

66

Vue 2.4 представил способ простой передачи событий вверх по иерархии с помощью vm.$listeners

Из https://vuejs.org/v2/api/#vm-listeners :

Содержит v-onпрослушиватели событий родительской области (без .nativeмодификаторов). Это может быть передано внутреннему компоненту через v-on="$listeners"- полезно при создании прозрачных компонентов оболочки.

См. Фрагмент ниже, используемый v-on="$listeners"в grand-childкомпоненте в childшаблоне:

Vue.component('parent', {
  template:
    '<div>' +
      '<p>I am the parent. The value is {{displayValue}}.</p>' +
      '<child @toggle-value="toggleValue"></child>' +
    '</div>',
  data() {
    return {
      value: false
    }
  },
  methods: {
    toggleValue() { this.value = !this.value }
  },
  computed: {
    displayValue() {
      return (this.value ? "ON" : "OFF")
    }
  }
})

Vue.component('child', {
  template:
    '<div class="child">' +
      '<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
      '<grand-child v-on="$listeners"></grand-child>' +
    '</div>'
})

Vue.component('grand-child', {
  template:
    '<div class="child">' +
      '<p>I am the grand-child: ' +
        '<button @click="emitToggleEvent">Toggle the value</button>' +
      '</p>' +
    '</div>',
  methods: {
    emitToggleEvent() { this.$emit('toggle-value') }
  }
})

new Vue({
  el: '#app'
})
.child {
  padding: 10px;
  border: 1px solid #ddd;
  background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <parent></parent>
</div>

Майкл Раш
источник
Я обнаружил, что это самый чистый способ без использования vuex / bus / root. Это передаст всю функцию испускания вверх. Вы все еще можете подключиться к детской эмиссии, установив @ someEmit = "someEmitHandler" перед v-on = "$ listeners"
Матиас Хаугсбо,
39

Сообщество Vue обычно поддерживает использование Vuex для решения такого рода проблем. Изменения вносятся в состояние Vuex, и представление DOM просто вытекает из этого, устраняя необходимость в событиях во многих случаях.

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

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


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

В grand-child,

methods: {
    doEvent() { 
        try {
            this.$el.dispatchEvent(new Event("eventtriggered"));
        } catch (e) {
            // handle IE not supporting Event constructor
            var evt = document.createEvent("Event");
            evt.initEvent("eventtriggered", true, false);
            this.$el.dispatchEvent(evt);
        }
    }
}

и parent,

mounted(){
    this.$el.addEventListener("eventtriggered", () => this.performAction())
}

В противном случае - да, придется переиздать или воспользоваться автобусом.

Примечание. Я добавил код в метод doEvent для обработки IE; этот код можно извлечь многоразовым способом.

Берт
источник
Это по-другому ведет себя для IE? не знал, что есть несоответствия браузера с vue ...
BassMHL 07
@BassemLhm Vue подходит для IE. Проблема с IE не в Vue, это решение DOM, и вы не можете использовать new Event () в IE. Вам нужно document.createEvent (). При необходимости я могу добавить поддержку IE.
Берт
Нет смысла устанавливать vuex только на один простой случай.
Адам Орлов
@AdamOrlov Я согласен с вами.
Берт
Более простое решение: stackoverflow.com/a/55650245/841591
digout
28

НОВЫЙ ОТВЕТ (обновление за ноябрь 2018 г.)

Я обнаружил, что мы действительно можем сделать это, используя $parentсвойство в большом дочернем компоненте:

this.$parent.$emit("submit", {somekey: somevalue})

Намного чище и проще.

BassMHL
источник
16
Обратите внимание, что это работает, только если отношения дочерние -> дедушки и бабушки. Это не работает, если дочерний элемент может быть вложен произвольно на глубину.
Qtax 04
См. Мой ответ stackoverflow.com/a/55650245/841591 в ответ на комментарий от @Qtax
digout
7
Вы же не хотите, чтобы в вашем большом проекте происходило подобное. Вы помещаете «потомок» в какой- transitionлибо другой компонент-оболочку, и он сломается, оставив вам большой вопросительный знак в вашей голове.
Адам Орлов
@AdamOrlov Я согласен, это плохая практика. Пожалуйста, обрабатывайте подобные события, используя хранилище Vuex.
Фабиан фон Эллертс 05
Это проще: stackoverflow.com/a/55650245/841591
Digout
26

Да вы правы, события переходят только от ребенка к родителю. Они не идут дальше, например, от ребенка к бабушке или дедушке.

В документации по Vue (кратко) эта ситуация рассматривается в разделе « Общение между родителями и детьми ».

Общая идея состоит в том, что в компоненте дедушка и бабушка вы создаете пустой Vueкомпонент, который передается от бабушки и дедушки к детям и внукам через свойства. Затем дедушка и бабушка слушают события, а внуки излучают события на этой «шине событий».

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

Вот пример того, как реализовать простую глобальную шину событий .

Sly_cardinal
источник
16

Другое решение будет включено / излучать на корневом узле:

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

Обновлено : иногда вы хотите отключить прослушиватель в определенных ситуациях, используйте vm. $ Off (например: vm.$root.off('event-name')inside lifecycle hook = beforeDestroy ).

Vue.component('parent', {
  template: '<div><button @click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 1,
      eventEnable: false
    }
  },
  created: function () {
    this.addEventListener()
  },
  beforeDestroy: function () {
    this.removeEventListener()
  },
  methods: {
    performAction() { this.action += 1 },
    toggleEventListener: function () {
      if (this.eventEnable) {
        this.removeEventListener()
      } else {
        this.addEventListener()
      }
    },
    addEventListener: function () {
      this.$root.$on('eventtriggered1', () => {
        this.performAction()
      })
      this.eventEnable = true
    },
    removeEventListener: function () {
      this.$root.$off('eventtriggered1')
      this.eventEnable = false
    }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child @eventtriggered="doEvent"></grand-child></div>',
  methods: {
    doEvent() { 
    	//this.$emit('eventtriggered') 
    }
  }
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Emit Event</button></div>',
  methods: {
    doEvent() { this.$root.$emit('eventtriggered1') }
  }
})

new Vue({
  el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<div id="app">
  <parent></parent>
</div>

Сфинкс
источник
Самое элегантное решение для меня.
Ovilia
Хорошее решение! Это хорошо сработало для меня в основном серверном приложении (Laravel) с несколькими компонентами Vue (я не хотел вводить уровень Vuex). Моей конкретной проблемой был компонент настраиваемой кнопки, который запускал модальное окно Buefy с компонентом формы. После того, как форма была отправлена, я хотел отключить кнопку, но пользовательские события, испускаемые из формы, не доходили до кнопки, поскольку модальное окно было прямым родительским элементом формы.
Ричард
Это не уничтожает слушателя при уничтожении. Это нормально? Было бы здорово, если бы вы могли добавить эту логику, поэтому я тоже знаю, как это сделать.
mesqueeb
14

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

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
выкопать
источник
1
Это правильный ответ. Я создал служебную функцию, чтобы сделать это в нескольких местах моего кода: spreadEvent (component, eventName, value) {while (component) {component. $ Emit (eventName, value); компонент = компонент. $ родительский; }}
ccleve 03
2
Это элегантный ответ, очень простой в использовании и легко читаемый. И это не нарушается добавлением дополнительных уровней обертки.
Дин
4

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

Во-первых : создайте файл js (я называю его eventbus.js) с таким содержимым:

import Vue from 'vue'    
Vue.prototype.$event = new Vue()

Во-вторых : в вашем дочернем компоненте испустите событие:

this.$event.$emit('event_name', 'data to pass')

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

this.$event.$on('event_name', (data) => {
  console.log(data)
})

Примечание. Если вы больше не хотите это событие, отмените его регистрацию:

this.$event.$off('event_name')

ИНФОРМАЦИЯ: Нет необходимости читать нижеприведенное личное мнение

Мне не нравится использовать vuex для общения между внучками и дедушками (или на аналогичном уровне общения).

В vue.js для передачи данных от прародителя к внучатому ребенку можно использовать provide / inject . Но в обратном нет ничего похожего. (внучка дедушке) Так что я использую шину событий всякий раз, когда мне нужно сделать такое общение.

Роли Роли
источник
2

Я сделал небольшой миксин на основе ответа @digout. Вы хотите поместить его перед инициализацией вашего экземпляра Vue (новый Vue ...), чтобы использовать его глобально в проекте. Вы можете использовать его как обычное событие.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
Кубакламча
источник
это решение я использовал для своей реализации, но я добавил дополнительный параметр, targetRefкоторый останавливает распространение на компоненте, на который вы нацеливаетесь. Тогда whileусловие будет включать && vm.$refs[targetRef]- вам также нужно будет включить этот refатрибут в целевой компонент. В моем случае использования мне не нужно было полностью туннелировать до root, сохраняя несколько событий от запуска и, возможно, пару драгоценных наносекунд время
mcgraw
2

Компоненты VueJS 2 имеют $parentсвойство, содержащее их родительский компонент.

Этот родительский компонент также включает собственное $parentсвойство.

Затем, доступ к компоненту «дедушка и бабушка», это вопрос доступа к компоненту «родительский родитель»:

this.$parent["$parent"].$emit("myevent", { data: 123 });

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

Роджервила
источник
1

Отказавшись от ответов @kubaklam и @ digout, это то, что я использую, чтобы избежать излучения на каждом родительском компоненте между внуком и (возможно, далекими) дедушкой и бабушкой:

{
  methods: {
    tunnelEmit (event, ...payload) {
      let vm = this
      while (vm && !vm.$listeners[event]) {
        vm = vm.$parent
      }
      if (!vm) return console.error(`no target listener for event "${event}"`)
      vm.$emit(event, ...payload)
    }
  }
}

При создании компонента с удаленными внуками, где вы не хотите, чтобы многие / какие-либо компоненты были привязаны к хранилищу, но хотите, чтобы корневой компонент действовал как хранилище / источник истины, это работает довольно хорошо. Это похоже на философию Ember по уменьшению данных. Обратной стороной является то, что если вы хотите прослушивать это событие для каждого родителя между ними, это не сработает. Но тогда вы можете использовать $ propogateEmit, как в ответе @kubaklam выше.

Изменить: начальный vm должен быть установлен для компонента, а не для его родителя. Т.е. let vm = thisа неlet vm = this.$parent

Ник
источник
0

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

window.Event = new class {

    constructor() {
        this.vue = new Vue();
    }

    fire(event, data = null) {
        this.vue.$emit(event, data);
    }

    listen() {
        this.vue.$on(event, callback);  
    }

}

Теперь вы можете просто запустить / транслировать / что угодно из любого места, позвонив:

Event.fire('do-the-thing');

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

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
Fylzero
источник
1
Я настоятельно рекомендую не присоединять случайные свойства к объекту окна, поскольку очень легко перезаписать существующие свойства или вступить в конфликт с существующими сторонними библиотеками. Вместо этого любой, кто использует Vue для решения этой проблемы, должен вместо этого использовать ответ @roli roli
HMilbradt
1
Я не уверен, что полностью понимаю или согласен с этим беспокойством. Привязка к прототипу - прекрасный подход, но привязка к окну является таким же, если не более распространенным, и, вероятно, более стандартным способом решения этой проблемы. Вы называете свойство, чтобы избежать конфликтов именования. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Это также предлагаемое решение, которое Джефф Уэй использует для Laracast. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero