【问题标题】:VueJs 2.0 emit event from grand child to his grand parent componentVueJs 2.0 将事件从孙子发送到他的祖父组件
【发布时间】:2019-01-10 11:30:00
【问题描述】:

似乎 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/ 的问题,但是通过 emtting 两个事件:

  • 一个从孙子到中间组件
  • 然后再次从中间组件发射到祖父组件

添加此中间事件似乎是重复且不必要的。有没有办法直接发射给我不知道的祖父母?

【问题讨论】:

    标签: javascript vuejs2 vue.js


    【解决方案1】:

    是的,你是正确的事件只会从孩子到父母。他们不会走得更远,例如从孩子到祖父母。

    Vue 文档(简要)在 Non Parent-Child Communication 部分解决了这种情况。

    一般的想法是在祖父组件中创建一个空的Vue 组件,该组件通过道具从祖父传递给子孙。然后,祖父母监听事件,孙辈在“事件总线”上发出事件。

    一些应用程序使用全局事件总线而不是每个组件的事件总线。使用全局事件总线意味着您需要有唯一的事件名称或命名空间,这样事件就不会在不同组件之间发生冲突。

    这是how to implement a simple global event bus 的示例。

    【讨论】:

      【解决方案2】:

      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 存在浏览器差异...
      • @BassemLhm Vue 适合 IE。 IE 的问题不是 Vue,这是一个 DOM 解决方案,你不能在 IE 中执行 new Event()。你必须 document.createEvent()。如果需要,我可以添加 IE 支持。
      • 只为一个简单的案例安装 vuex 是没有意义的。
      • @AdamOrlov 我同意你的看法。
      • 更简单的解决方案:stackoverflow.com/a/55650245/841591
      【解决方案3】:

      另一个解决方案将在 root 节点开启/发射:

      grand-child中使用vm.$root.$emit,然后在祖先(或任何你想要的地方)使用vm.$root.$on

      更新:有时您想在某些特定情况下禁用监听器,请使用vm.$off(例如:vm.$root.off('event-name') inside生命周期钩子=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>

      【讨论】:

        【解决方案4】:

        新答案(2018 年 11 月更新)

        我发现我们实际上可以通过利用大子组件中的$parent 属性来做到这一点:

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

        更简洁。

        【讨论】:

        • 请注意,这仅适用于关系为子 -> 祖父母的情况。如果子级可以嵌套任意层深,这不起作用
        • 查看我对@Qtax 评论的回复stackoverflow.com/a/55650245/841591
        • 您不希望在您的大项目中发生这种事情。您将“孩子”放在transition 或任何其他包装器组件中,它会损坏,并在您的脑海中留下一个大问号。
        • @AdamOrlov 我同意,这是不好的做法。请使用 Vuex 商店处理此类事件。
        • 真的是最好的办法!
        【解决方案5】:

        这是我使用event bus的唯一情况!!用于将数据从深层嵌套子级传递到非直接父级通信。

        首先:用这个内容创建一个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')
        

        INFO:无需阅读以下个人意见

        我不喜欢使用 vuex 进行孙子与祖父之间的通信(或类似的通信级别)。

        在 vue.js 中用于将数据从祖父母传递给孙子女,您可以使用 provide/inject。但是相反的东西没有类似的东西。 (孙子到祖父)所以每当我必须进行这种通信时,我都会使用事件总线。

        【讨论】:

          【解决方案6】:

          如果您想灵活地简单地向所有父母及其父母递归地广播一个事件直到根,您可以执行以下操作:

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

          【讨论】:

            【解决方案7】:

            Vue 2.4 引入了一种使用 vm.$listeners 轻松将事件向上传递的方法

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

            包含父作用域v-on 事件侦听器(没有.native 修饰符)。这可以通过v-on="$listeners" 传递给内部组件 - 在创建透明包装组件时很有用。

            child模板的grand-child组件中使用v-on="$listeners"查看下面的sn-p:

            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>

            【讨论】:

              【解决方案8】:

              我真的很喜欢通过创建一个绑定到窗口的类并简化广播/侦听设置以在 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!');
              });
              

              【讨论】:

              • 我强烈建议不要将随机属性附加到窗口对象,因为它很容易覆盖现有属性或与现有的第 3 方库发生冲突。相反,任何使用 Vue 来解决这个问题的人都应该改用 @roli roli 的答案
              • 我不确定我是否完全理解或同意这种担忧。绑定到原型是一种很好的方法,但绑定到窗口是常见的,甚至可能是更标准的处理方式。您为属性命名,因此很容易避免命名冲突。 medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… 这也是 Jeff Way 在 Laracasts 上使用的建议解决方案。 laracasts.com/series/learn-vue-2-step-by-step/episodes/13
              【解决方案9】:

              我根据@digout 的回答做了一个简短的 mixin。你想把它放在你的 Vue 实例初始化(新的 Vue ...)之前,以便在项目中全局使用它。您可以像正常事件一样使用它。

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

              【讨论】:

              • 这个解决方案是我在实现中使用的,但我添加了一个额外的参数targetRef,它会停止在您定位的组件上的传播。然后while 条件将包含&amp;&amp; vm.$refs[targetRef] - 您还需要在目标组件上包含ref 属性 在我的用例中,我不需要一直隧道到根,节省了一些事件的触发时间,也许还有几个宝贵的纳秒时间
              【解决方案10】:

              引用@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 的数据向下操作哲学。不利的一面是,如果您想在其间的每个父母身上监听该事件,那么这将行不通。但是你可以像上面@kubaklam的回答一样使用$propogateEmit。

              编辑:初始 vm 应该设置为组件,而不是组件的父级。 IE。 let vm = this 而不是 let vm = this.$parent

              【讨论】:

                【解决方案11】:

                VueJS 2 组件有一个 $parent 属性,其中包含其父组件。

                该父组件还包括其自己的$parent 属性。

                然后,访问“祖父母”组件就是访问“父母的父”组件:

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

                无论如何,这有点棘手,我建议使用像 Vuex 这样的全局状态管理器或类似工具,正如其他响应者所说的那样。

                【讨论】:

                • 这对于可以是子或孙子的组件来说不是很好。
                • 正如我所说,这个解决方案是一个技巧。正如其他响应者所说,我建议使用像 Vuex 这样的全局状态管理器或类似工具。
                • Michael Rush 的回答更适合这些情况。它避免了创建方法只是为了在祖先链上发回相同的消息。
                【解决方案12】:

                Vue 3 开始,根事件发生了一些根本性的变化:

                $on$off$once 根方法不再存在。在一定程度上有一些东西可以替代它,因为你可以通过这样做listen to root events

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

                另一种解决方案是事件总线,但 Vue.js 文档的看法很模糊 - 从长远来看,它们可能会导致维护问题。您可能会得到一组不断扩展的发射和事件接收器,但对如何管理它或哪些组件可能在其他地方受到影响没有明确或核心的想法。尽管如此,事件总线文档给出的例子是mitttiny-emitter

                但是文档明确表示他们建议按以下顺序处理这些情况:

                • 道具方便家长/孩子交流的解决方案。
                • 提供/注入一种简单的祖先与后代交流的方式(尽管很重要,而不是相反)。
                • Vuex 一种以清晰的方式处理全局状态的方法。请务必注意,这不仅仅用于事件或通信 - Vuex 主要用于处理状态。

                对于 OP 的选择基本上归结为使用事件总线或 Vuex。为了集中事件总线,你可以把它放在 Vuex 中,如果状态也需要全局可用的话。否则,使用对其行为和位置进行严格集中控制的事件总线可能会有所帮助。

                【讨论】:

                  【解决方案13】:

                  模仿@digout 的答案。我在想,如果目的是将数据发送给远祖,那么我们根本不需要 $emit。我为我的边缘案例做了这个,它似乎工作。是的,它可以通过 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
                      }
                    }
                  }
                  

                  给定一个祖先:

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

                  曾孙说

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

                  【讨论】:

                    猜你喜欢
                    • 2023-01-07
                    • 2022-08-02
                    • 2018-08-19
                    • 1970-01-01
                    • 1970-01-01
                    • 2019-11-23
                    相关资源
                    最近更新 更多