VueJs 2.0将事件从子级发送到其子级组件

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/,但是通过发出两个事件:

  • 一个从孙子到中间部分
  • 然后再从中间组分向外祖父母发射

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

85369 次浏览

是的,你是对的,事情只会从孩子传到父母身上,而不会更进一步,比如从孩子传到祖父母。

Vue 文档(简要地)在 非亲子沟通小节中讨论了这种情况。

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

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

下面是 如何实现一个简单的全局事件总线的一个例子。

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 的代码; 这些代码可以以可重用的方式提取。

另一个解决方案将在 节点处打开/发出:

孙子中使用 vm.$root.$emit,然后在祖先(或任何您想要的地方)使用 vm.$root.$on

更新 : 有时您希望在某些特定情况下禁用侦听器,请使用 关闭(例如: 生命周期 hook = 在毁灭之前中的 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>

最新答案(2018年11月更新)

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

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

更干净,更简单。

这是唯一的情况下,我使用 活动巴士! !用于将数据从深嵌套的子级传递到非直接的父级,通信。

首先 : 创建一个包含以下内容的 js 文件(我将其命名为 eventbus.js) :

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

Second : 在子组件中发出一个事件:

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 中,可以使用 提供/注射将数据从祖父母传递给子孙。但是,对于相反的事物,却没有类似的东西。(孙子对孙父)所以我用事件总线每当我必须做这种沟通。

如果您希望灵活一些,只需递归地向所有父级和他们的父级广播一个事件到根,那么您可以这样做:

let vm = this.$parent


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

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

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

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

请参阅下面使用 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>

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

我已经做了一个简短的混合基于@digout 答案。您希望将它放在 Vue 实例初始化(new Vue...)之前,以便在项目中全局使用它。你可以像使用普通事件一样使用它。

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

重复一下@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 的数据向下行动哲学相似。不好的一面是,如果你想监听每一个父母在这之间的事件,那么这不会工作。但是你可以使用 $propoateEmit 作为上面的回答@kubaklam。

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

VueJS2组件具有包含其父组件的 $parent属性。

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

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

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

无论如何,这是一种 很狡猾,我建议使用全球状态管理器,如 Vuex 或类似的工具,正如其他应答者所说。

至于 Vue 3,根本事件已经发生了一些根本性的变化:

$on$off$once根方法不再存在。在某种程度上,有一些东西可以代替它,因为你可以通过这样做 监听根事件:

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

另一种解决方案是事件总线,但是 Vue.js 文档的观点比较模糊——从长远来看,它们会造成维护方面的麻烦。您可能会得到一组不断扩散的发射和事件接收器,但对于如何管理它或哪些组件可能在其他地方受到影响没有明确或核心的想法。尽管如此,事件总线的文档给出的例子是 Mitt微型发射器

然而,医生明确表示,他们建议按以下顺序处理这类情况:

  • 一个方便父母与子女沟通的解决方案。
  • 提供/注入 一种简单的方式让祖先与他们的后代交流(尽管很关键,但不是反过来)。
  • 一种以清晰的方式处理全球状态的方法。需要注意的是,这不仅仅是为了事件或者通信—— Vuex 主要是为了处理状态而构建的。

本质上,OP 的选择可以归结为使用事件总线(event bus)或 Vuex。为了集中活动总线,你可以把它放在 Vuex 内部,如果国家也需要全球可用。否则,使用对其行为和位置进行严格集中控制的事件总线可能会有所帮助。

即兴发挥@digout 答案。我在想,如果目的是将数据发送给远古祖先,那么我们根本不需要 $胚胎。我这么做是为了我的优势,而且看起来很有效。是的,它可以通过一个 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'
})