简体   繁体   English

VueJs 2.0 将事件从孙子发送到他的祖父组件

[英]VueJs 2.0 emit event from grand child to his grand parent component

It seems that Vue.js 2.0 doesn't emit events from a grand child to his grand parent component. 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'
})

This JsFiddle solves the issue https://jsfiddle.net/y5dvkqbd/4/ , but by emtting two events:这个 JsFiddle 解决了问题https://jsfiddle.net/y5dvkqbd/4/ ,但是通过 emtting 两个事件:

  • One from grand child to middle component一个从孙子到中间组件
  • Then emitting again from middle component to grand parent然后再次从中间组件发射到祖父母

Adding this middle event seems repetitive and unneccessary.添加这个中间事件似乎是重复的和不必要的。 Is there a way to emit directly to grand parent that I am not aware of?有没有办法直接发射给我不知道的祖父母?

Vue 2.4 introduced a way to easily pass events up the hierarchy using vm.$listeners Vue 2.4 引入了一种使用vm.$listeners轻松将事件向上传递的方法

From https://v2.vuejs.org/v2/api/#vm-listeners :来自https://v2.vuejs.org/v2/api/#vm-listeners

Contains parent-scope v-on event listeners (without .native modifiers).包含父范围的v-on事件侦听器(没有.native修饰符)。 This can be passed down to an inner component via v-on="$listeners" - useful when creating transparent wrapper components.这可以通过v-on="$listeners"传递给内部组件 - 在创建透明包装组件时很有用。

See the snippet below using v-on="$listeners" in the grand-child component in the child template:child模板grand-child组件中使用v-on="$listeners"查看下面的代码片段:

 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>

NEW ANSWER (Nov-2018 update)新答案(2018 年 11 月更新)

I discovered that we could actually do this by leveraging the $parent property in the grand child component:我发现我们实际上可以通过利用大子组件中的$parent属性来做到这一点:

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

Much cleaner and simpler.更干净,更简单。

The Vue community generally favors using Vuex to solve this kind of issue. Vue 社区普遍倾向于使用 Vuex 来解决此类问题。 Changes are made to Vuex state and the DOM representation just flows from that, eliminating the need for events in many cases.对 Vuex 状态进行了更改,并且 DOM 表示只是从中流出,在许多情况下消除了对事件的需求。

Barring that, re-emitting would probably be the next best choice, and lastly you might choose to use an event bus as detailed in the other highly voted answer to this question.除此之外,重新发射可能是下一个最佳选择,最后您可能会选择使用事件总线,如对该问题的另一个高度投票的答案中所详述。

The answer below is my original answer to this question and is not an approach I would take now, having more experience with Vue.下面的答案是我对这个问题的原始答案,而不是我现在会采用的方法,因为我对 Vue 有更多的经验。


This is a case where I might disagree with Vue's design choice and resort to DOM.这是我可能不同意 Vue 的设计选择并诉诸 DOM 的情况。

In grand-child ,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);
        }
    }
}

and in parent ,parent中,

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

Otherwise, yes, you have to re-emit, or use a bus.否则,是的,您必须重新发送或使用公共汽车。

Note: I added code in the doEvent method to handle IE;注意:我在doEvent方法中添加了处理IE的代码; that code could be extracted in a reusable way.该代码可以以可重用的方式提取。

Yes, you're correct events only go from child to parent.是的,你是正确的事件只从孩子到父母。 They don't go further, eg from child to grandparent.他们不会走得更远,例如从孩子到祖父母。

The Vue documentation (briefly) addresses this situation in the Non Parent-Child Communication section. Vue 文档(简要)在非父子通信部分解决了这种情况。

The general idea is that in the grandparent component you create an empty Vue component that is passed from grandparent down to the children and grandchildren via props.总体思路是在祖父组件中创建一个空的Vue组件,该组件通过 props 从祖父传递给子孙。 The grandparent then listens for events and grandchildren emit events on that "event bus".然后,祖父母侦听事件,而孙子女在该“事件总线”上发出事件。

Some applications use a global event bus instead of a per-component event bus.一些应用程序使用全局事件总线而不是每个组件的事件总线。 Using a global event bus means you will need to have unique event names or namespacing so events don't clash between different components.使用全局事件总线意味着您将需要具有唯一的事件名称或命名空间,以便事件不会在不同组件之间发生冲突。

Here is an example of how to implement a simple global event bus .下面是一个如何实现一个简单的全局事件总线的例子。

If you want to be flexible and simply broadcast an event to all parents and their parents recursively up to the root, you could do something like:如果您想灵活地简单地将事件广播给所有父母及其父母,直到根,您可以执行以下操作:

let vm = this.$parent

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

Another solution will be on/emit at root node:另一种解决方案将在根节点开启/发射:

Uses vm.$root.$emit in grand-child , then uses vm.$root.$on at the ancestor (or anywhere you'd like).grand-child中使用vm.$root.$emit ,然后在祖先(或您想要的任何地方)使用vm.$root.$on

Updated : sometimes you'd like to disable the listener at some specific situations, use vm.$off (for example: vm.$root.off('event-name') inside lifecycle hook= beforeDestroy ).更新:有时您想在某些特定情况下禁用侦听器,请在生命周期 hook= beforeDestroy中使用vm.$off (例如: vm.$root.off('event-name') )。

 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>

This is the only case when I use event bus !!这是我使用事件总线的唯一情况! For passing data from deep nested child, to not directly parent, communication.用于将数据从深层嵌套子级传递到非直接父级通信。

First : Create a js file (I name it eventbus.js) with this content:首先:使用以下内容创建一个 js 文件(我将其命名为 eventbus.js):

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

Second : In your child component emit an event:第二:在您的子组件中发出一个事件:

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

Third : In the parent listen to that event:第三:在父母听那个事件:

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

Note: If you don't want that event anymore please unregister it:注意:如果您不再想要该活动,请取消注册:

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

INFO: No need to read the below personal opinion信息:无需阅读以下个人意见

I don't like to use vuex for grand-child to grand-parent communication (Or similar communication level).我不喜欢使用 vuex 进行孙子与祖父母的交流(或类似的交流级别)。

In vue.js for passing data from grand-parent to grand-child you can use provide/inject .在 vue.js 中用于将数据从祖父母传递给孙子女,您可以使用提供/注入 But there is not something similar for the opposite thing.但是相反的东西没有类似的东西。 (grand-child to grand-parent) So I use event bus whenever I have to do that kind of communication. (孙子到祖父)所以每当我必须进行这种通信时,我都会使用事件总线。

I've made a short mixin based on @digout answer.我根据@digout 的回答做了一个简短的混合。 You want to put it, before your Vue instance initialization (new Vue...) to use it globally in project.你想把它放在你的 Vue 实例初始化(新的 Vue ...)之前,以便在项目中全局使用它。 You can use it similarly to normal event.您可以像正常事件一样使用它。

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})

VueJS 2 components have a $parent property that contains their parent component. VueJS 2 组件有一个包含其父组件的$parent属性。

That parent component also includes its own $parent property.该父组件还包括其自己的$parent属性。

Then, accessing the "grandparent" component it's a matter of accessing the "parent's parent" component:然后,访问“祖父母”组件就是访问“父母的父”组件:

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

Anyway, this is kinda tricky , and I recommend using a global state manager like Vuex or similar tools, as other responders have said.无论如何,这有点棘手,我建议使用像 Vuex 这样的全局状态管理器或类似工具,正如其他响应者所说的那样。

Riffing off @kubaklam and @digout's answers, this is what I use to avoid emitting on every parent component between the grand-child and the (possibly distant) grandparent:引用@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)
    }
  }
}

When building out a component with distant grand children where you don't want many/any components to be tied to the store, yet want the root component to act as a store/source of truth, this works quite well.当构建一个包含遥远的孙子的组件时,您不希望将许多/任何组件绑定到存储,但希望根组件充当存储/事实来源,这非常有效。 This is similar to the data down actions up philosophy of Ember.这类似于 Ember 的数据向下操作哲学。 Downside is that if you want to listen for that event on every parent in between, then this won't work.不利的一面是,如果您想在其间的每个父母身上监听该事件,那么这将行不通。 But then you can use $propogateEmit as in above answer by @kubaklam.但是你可以像上面@kubaklam 的回答一样使用 $propogateEmit 。

Edit: initial vm should be set to the component, and not the component's parent.编辑:初始 vm 应该设置为组件,而不是组件的父级。 Ie let vm = this and not let vm = this.$parentlet vm = this而不是let vm = this.$parent

I really dig the way this is handled by creating a class that is bound to the window and simplifying the broadcast/listen setup to work wherever you are in the Vue app.我通过创建一个绑定到窗口的类并简化广播/侦听设置以在 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);  
    }

}

Now you can just fire / broadcast / whatever from anywhere by calling:现在你可以通过调用从任何地方发射/广播/任何东西:

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

...and you can listen in a parent, grandparent, whatever you want by calling: ...您可以通过以下方式聆听父母,祖父母的任何声音:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});

As of Vue 3 , a number of fundamental changes have happened to root events:Vue 3开始,根事件发生了一些根本性的变化:

The $on , $off and $once root methods no longer exist. $on$off$once根方法不再存在。 There is to a certain extent something to replace this, since you can listen to root events by doing this:在一定程度上有一些东西可以替代它,因为您可以通过这样做来监听根事件

createApp(App, {
  // Listen for the 'expand' event
  onExpand() {
    console.log('expand')
  }
})

Another solution are event buses, but the Vue.js documents take a dim view - they can cause maintenance headaches in the long run.另一个解决方案是事件总线,但 Vue.js 文档的观点很模糊——从长远来看,它们可能会导致维护问题。 You might get an ever spreading set of emits and event sinks, with no clear or central idea of how it is managed or what components could be affected elsewhere.您可能会得到一组不断扩展的发射和事件接收器,但对于如何管理它或哪些组件可能在其他地方受到影响没有明确或核心的想法。 Nonetheless, examples given by the docs of event buses are mitt and tiny-emitter .尽管如此,事件总线文档给出的例子是mitttiny-emitter

However the docs make it clear that they recommend handling these sorts of situations in this order:但是,文档明确表示他们建议按以下顺序处理这些情况:

  • Props A convenient solution for parent / child communications.道具亲子交流的便捷解决方案。
  • Provide/Inject A simple way for ancestors to communicate with their descendants (although critically, not the other way around).提供/注入一种简单的方式让祖先与他们的后代进行交流(尽管很重要,而不是相反)。
  • Vuex A way of handling global state in a clear fashion. Vuex一种以清晰的方式处理全局状态的方法。 It's important to note that this is not solely for events, or communications - Vuex was built primarily to handle state.重要的是要注意,这不仅仅用于事件或通信 - Vuex 主要是为处理状态而构建的。

Essentially the choice for the OP would come down to using an event bus, or Vuex.本质上,OP 的选择归结为使用事件总线或 Vuex。 In order to centralise the event bus, you could place it inside Vuex, if state was also needed to be globally available.为了集中事件总线,你可以把它放在 Vuex 中,如果状态也需要全局可用的话。 Otherwise using an event bus with strict centralised controls on it's behaviour and location might help.否则,使用对其行为和位置进行严格集中控制的事件总线可能会有所帮助。

Riffing off @digout answer.扯掉@digout的答案。 I am thinking that if the purpose is to send data to a far-ancestor then we don't need $emit at all.我在想,如果目的是将数据发送给远祖,那么我们根本不需要 $emit。 I did this for my edge-case and it seems to work.我为我的边缘案例做了这个,它似乎工作。 Yes, it could be implemented via a mixin but it doesn't have to be.是的,它可以通过 mixin 来实现,但不是必须的。

/**
 * Send some content as a "message" to a named ancestor of the component calling this method.
 * This is an edge-case method where you need to send a message many levels above the calling component.
 * Your target component must have a receiveFromDescendant(content) method and it decides what
 * to do with the content it gets.
 * @param {string} name - the name of the Vue component eg name: 'myComponentName'
 * @param {object} content - the message content
 */
messageNamedAncestor: function (name, content) {
  let vm = this.$parent
  let found = false
  while (vm && !found) {
    if (vm.$vnode.tag.indexOf('-' + name) > -1) {
      if (vm.receiveFromDescendant) {
        found = true
        vm.receiveFromDescendant(content)
      } else {
        throw new Error(`Found the target component named ${name} but you dont have a receiveFromDescendant method there.`)
      }
    } else {
      vm = vm.$parent
    }
  }
}

Given an ancestor:给定一个祖先:

export default {
  name: 'myGreatAncestor',
  ...
  methods: {
     receiveFromDescendant (content) {
        console.log(content)
     }
   }
}

A great grand-child says一个曾孙说

// Tell the ancestor component something important
this.messageNamedAncestor('myGreatAncestor', {
  importantInformation: 'Hello from your great descendant'
})

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM