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/ , але викидаючи дві події:

  • Один від великої дитини до середнього компонента
  • Потім знову випромінює від середнього компонента до великого батька

Додавання цієї середньої події видається повторюваним і непотрібним. Чи є спосіб прямого випромінювання до великого батька, про який я не знаю?

Відповіді:


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

@BassemLhm Vue чудово працює з IE. Проблема IE не у Vue, це рішення DOM, і ви не можете зробити новий Event () в IE. Ви повинні document.createEvent (). Я можу додати підтримку IE, якщо це потрібно.
Берт

Немає сенсу встановлювати vuex лише для одного простого випадку.
Адам Орлов,

@AdamOrlov Я з вами згоден.
Берт,

Більш просте рішення: stackoverflow.com/a/55650245/841591
ЦфВи

28

НОВА ВІДПОВІДЬ (оновлення від листопада 2018 р.)

Я виявив, що насправді ми можемо зробити це, використовуючи $parentвластивість у великому дочірньому компоненті:

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

Набагато чистіше і простіше.


16
Зверніть увагу, що це працює лише в тому випадку, якщо стосунки є дочірніми -> бабуся і дідусь. Це не працює, якщо в дитину можна вкладати довільні рівні глибоко.
Qtax

Дивіться мою відповідь stackoverflow.com/a/55650245/841591 у відповідь на коментар від @Qtax
digout

7
Ви не хочете, щоб подібні речі відбувалися у вашому великому проекті. Ви поміщаєте "дитину" в transitionбудь-який інший компонент обгортки, і він зламається, залишаючи у вас великий знак питання в голові.
Адам Орлов,

@AdamOrlov Я згоден, це погана практика. Будь ласка, обробляйте подібні події за допомогою магазину Vuex.
Фабіан фон Еллерц


26

Так, ви правильні події переходять лише від дитини до батьків. Вони не йдуть далі, наприклад, від дитини до бабусі та дідуся.

Документація Vue (коротко) розглядає цю ситуацію в розділі " Немає батьків та дітей ".

Загальна ідея полягає в тому, що в компоненті «бабуся і дідусь» ви створюєте порожній Vueкомпонент, який передається від дідуся та бабусі дітям та онукам через реквізит. Потім дідусь і бабуся слухає події, а онуки випускають події в цьому "автобусі подій".

Деякі програми використовують глобальну шину подій замість шини подій для кожного компонента. Використання глобальної шини подій означає, що вам потрібно мати унікальні імена подій або простір імен, щоб події не зіштовхувались між різними компонентами.

Ось приклад того, як реалізувати просту глобальну шину подій .


16

Іншим рішенням буде ввімкнення / випромінювання в кореневому вузлі:

Використовується vm.$root.$emitу внучку , потім використовує vm.$root.$onу предка (або де завгодно).

Оновлено : іноді ви хочете вимкнути слухач у деяких конкретних ситуаціях, використовуйте vm. $ Off (наприклад: vm.$root.off('event-name')усередині життєвого циклу гачок = 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>


Найелегантніше рішення для мене.
Овілія

Гарне рішення! Це добре працювало для мене в основному на серверній програмі (Laravel) з кількома компонентами Vue (я не хотів вводити рівень Vuex). Моєю конкретною проблемою був користувальницький компонент кнопки, який ініціював модаль Buefy із компонентом форми. Після того, як форма була надіслана, я хотів відключити кнопку, але користувацькі події, випущені з форми, не досягли цієї кнопки, оскільки Modal був безпосереднім батьківським елементом форми.
Річард

Це не знищує слухача на знищення. Це нормально? Було б чудово, якби ви могли додати цю логіку, тому я знаю, як це зробити.
mesqueeb

14

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

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}

1
Це правильна відповідь. Я створив службову функцію, яка робить це в кількох місцях мого коду: propagateEvent (компонент, ім'я події, значення) {while (компонент) {компонент. $ Emit (ім'я події, значення); компонент = компонент. $ батьківський; }}
ccleve

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 для передачі даних від прабатьків до онуків ви можете використовувати надання / ін'єкцію . Але немає чогось подібного для протилежного. (онук до бабусі) Тож я використовую шину подій, коли мені доводиться спілкуватися такого роду.


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атрибут до цільового компонента. У моєму випадку використання мені не потрібно було проходити тунель до кореня, рятуючи кілька подій від спрацьовування та, можливо, пару дорогоцінних наносекунд час
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)
    }
  }
}

Коли ви створюєте компонент із віддаленими дітьми, де ви не хочете, щоб багато / будь-які компоненти були прив'язані до магазину, але хочете, щоб кореневий компонент діяв як магазин / джерело істини, це працює досить добре. Це схоже на дані про дії вниз за філософією Ембера. Недоліком є ​​те, що якщо ви хочете прослухати цю подію у кожного з батьків між ними, то це не спрацює. Але тоді ви можете використовувати $ 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!');
});

1
Я настійно рекомендую не приєднувати до об'єкта вікна випадкові властивості, оскільки дуже легко переписати існуючі властивості або конфліктувати з існуючими сторонніми бібліотеками. Натомість, будь-хто, хто використовує Vue для вирішення цієї проблеми, повинен замість цього використовувати відповідь
@roli roli

1
Я не впевнений, що повністю розумію або погоджуюся з цим занепокоєнням. Прив’язка до прототипу є прекрасним підходом, але прив’язка до вікна є настільки ж, якщо не навіть більш загальним, і, можливо, більш стандартним способом обробки цього. Ви називаєте властивість, тому уникнути конфліктів імен просто. medium.com/@amitavroy7 / ... stackoverflow.com/questions/15008464 / ... Це також запропоноване рішення Jeff Way використовує на Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.