Vue组件通讯

发布于 大漠

使用Vue构建组件容易,但对于初学者要掌握Vue组件中的通讯还是有一定的难度。比如说,父组件如何向子组件通讯?子组件又是如何向父组件通讯?兄弟组件又是怎么通讯?这些方面都是有关于组件通讯相关的知识。而且掌握Vue组件之间的通讯方式还是掌握Vue组件的另一种能力。

在Vue中,Vue的组件自身就很棒,因为它可以帮助我们使用重用的代码片段,我们也可以使用Vue提供的所有功能。现在我们要掌握怎么建立组件之间的连接,也就是组件的通讯,以便一个组件中的操作可以更新应用程序中的其他组件。在接下来的内容中,咱们会涉及两个部分,第一个部分是父子组件怎么通讯;第二部分兄弟组件怎么通讯?

父子组件通讯

先来看看父子组件之间如何建立通讯:将看到父组件如何向子组件通讯和子组件如何向父组件通讯

父组件向子组件通讯

要在Vue中将数据从父组件传到子组件,我们可以通过 **props**来实现。在React中也是使用类似的约定来实现组件之间的数据共享。props指的是从外部设置的属性,例如来自父组件。为了告诉Vue子组件从自已实例的外部接收数据,需要在子组件的Vue对象中设置props属性。这个属性包含一个String数组,每个字符串表示一个可以从父组件设置的属性。请注意,props严格用于父组件与子组件之间的单向通讯,并且你不希望尝试直接在子组件中更改props的值。否则,你将收到类似这样的错误信息“避免直接修改某个prop,因为当父组件重新渲染时,该值将被覆盖” 这样的错误。相反,根据prop的值使用datacomputed

父组件使用属性绑定

为了将数据从父组件传到子组件,在父组件中设置一个属性,该属性绑定和子组件的prop相同的名称一个属性值。请注意,我们现在位于父组件内部,但是我们使用了自定义标签<child-card>来渲染子组件。在这个标记上,我们设置了绑定属性。在下面的示例中,使用parentMessage作为属性名,所以在子组件中需要一个props: ['parentMessage']作为一个prop。然后在父组件中,使用<child-card :parentMessage = "parentMessage"></child-card>来传递数据。

<!-- ParentCard.vue -->
<template>
    <div class="card">
        <div class="card-header">
            <h5 v-text="theCardTitle"></h5>
            <button @click="sendMessage" class="btn">给子组件发送一个消息</button>
        </div>
        <div class="card-body">
            <child-card :parentMessage="parentMessage"></child-card>
        </div>
    </div>
</template>

<script>
    import ChildCard from './ChildCard';

    export default {
        name: 'ParentCard',
        data: () => ({
            theCardTitle: '父组件',
            parentMessage: ''
        }),
        components: {
            ChildCard
        },
        methods: {
            sendMessage() {
                this.parentMessage = `<b>消息来自父组件:</b> (^_^)!!!`
            }
        }
    }
</script>

子组件使用props对象

接下来的代码中,创建一个ChildCard子组件,它的data中有一个props数组,并且设置了一个parentMessage字符串。这表明parentMessage可以从外部设置,也可以从父组件设置。这正是我们在上一节中所做的。为prop提供的字符串名称,在我们的示例中,parentMessage必须与此组件中模板部分中使用的属性名相匹配。

<!-- ChildCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text" v-html="theCardBody"></p>
            <div v-if="parentMessage" class="alert" v-html="parentMessage"></div>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'ChildCard',
        props: ['parentMessage'],
        data: () => ({
            theCardTitle: '子组件',
            theCardBody: '我是一个子组件!(^_^) !!!'
        })
    }
</script>

上面的两小节中,分别创建了ParentCardChildCard两个组件,而且子组件ChildCard嵌套在父组件ParentCard中。在子组件中,使用了v-if指令有条件地显示来自父组件ParentCard的消息,并且显示在div.alert中。如果没有消息,则不会显示.alert。因此,当页面首次渲染时,parentMessage的初始值是一个空字符串(在ParentCard组件的data中设置了parentMessage为空字符串)。所以我们一开始渲染页面的时候,并看不到(ChildCard子组件不显示.alert)。当用户点击了ParentCard组件中的“发送消息”的按钮时,则会触发ParentCard组件中定义的sendMessage()方法。这个时候,parentMessage的值就变成了<b>消息来自父组件:</b> (^_^)!!!。由于此变量使用:parentMessage="parentMessage"绑定到<child-card>标签上,并且子组件ChildCard通过props:['parentMessage']接受该值,如此一来,子组件将使用来自父组件的parentMessage的值。

最终效果如下:

当你点击示例中右上角的按钮,就可以看到父组件向子组件发送的消息,这样就完成了父组件向子组件的数据通讯:

简单的总结一下:

在Vue中,父组件向子组件传递数据(通讯),可以借助props属性完成。

可以用一张类似下面这样的图来描述父组件向子组件通讯的关系:

子组件向父组件通讯

事实除了从父组件向子组件传数据之外,有时候也要能从子组件向父组件传数据。那么问题来了,在Vue中如何从子组件向父组件传数据(通讯)?在Vue中要实现这个,我们可以在子组件中发出自定义事件,并在父组件中侦听发出的事件(子组件中自定义的事件)。我们在上面的示例上来做一些更改,完成一个子组件向父组件通讯的示例。

子组件发出自定义事件

首先在子组件ChildCard<template>中添加一个新的标签button。在这个button添加一个click事件:

<!-- ChildCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text" v-html="theCardBody"></p>
            <div v-if="parentMessage" class="alert" v-html="parentMessage"></div>
            <button v-if="parentMessage" @click="ok" class="btn">OK</button>
        </div>
    </div>
</template>

当我们点击“OK”按钮时,想运行名为ok()的方法。让我们设置该方法,以便在触发事件时发出($emit)自定义的事件。我们将一个finished字符串传递给$emit函数。当然我们可以选择自己喜欢的名字,但在这个案例中我们选择了finished这个名,这样更具语义化。我们希望子组件的信息发送到父组件。

<!-- ChildCard.vue -->
<script>
    export default {
        name: 'ChildCard',
        props: ['parentMessage'],
        data: () => ({
            theCardTitle: '子组件',
            theCardBody: '我是一个子组件!(^_^) !!!'
        }),
        methods: {
            ok() {
                this.$emit('finished')
            }
        }
    }
</script>

父组件侦听自定义事件

现在我们可以回到父组件ParentCard中,在父组件中使用自定义标签调用子组件ChildCard,在这个标签中我们可以使用@finished="finished"侦听子组件中自定义的事件。这意味着我们需要在父组件中定义一个finished()方法。父组件中定制了自定义属性的侦听器和触发它的方法。

<!-- ParentCard.vue -->
<template>
    <div class="card">
        <div class="card-header">
            <h5 v-text="theCardTitle"></h5>
            <button @click="sendMessage" class="btn">给子组件发送一个消息</button>
        </div>
        <div class="card-body">
            <child-card :parentMessage="parentMessage" @finished="finished"></child-card>
        </div>
    </div>
</template>

<script>
    import ChildCard from "./ChildCard";

    export default {
        name: "ParentCard",
        data: () => ({
            theCardTitle: "父组件",
            parentMessage: ""
        }),
        components: {
            ChildCard
        },
        methods: {
            sendMessage() {
                this.parentMessage = `<b>消息来自父组件:</b> (^_^)!!!`;
            },
            finished() {
                this.parentMessage = ''
            }
        }
    };
</script>

在这个示例中,用户首先点击“发送消息”按钮,它将消息向下发送到子组件,这个时候消息和一个新按钮会一起在子组件中渲染。

现在我们可点击“OK”按钮,它会向父组件发出自定义的finished事件。在父组件中,我们正在侦听该自定义事件,当侦听到finished自定义事件时,就会触发finished()方法,将parentMessage重置为空字符串。现在这个示例,我们实现了父组件向子组件和子组件向父组件传递数据(数据通讯)。

同样的,我们可以用张图来描述:

通过回调函数实现子组件向父组件通讯

上面的示例是通过自定义事件完成子组件向父组件进行数据通讯。如果你不想发出自定义事件,还可以通过另一种方式将消息从子组件发送到父组件。和上一个示例不同之处是,我们不需要在子组件中定义ok()方法,而是在父组件中定义该方法。一旦我们在父组件上定义了该方法,就可以通过props把信息从父组件传递给子组件。所以在ParentCard组件中定义ok()方法,并且在<child-card>上绑定已定义该方法和props

<!-- ParentCard.vue -->
<template>
    <div class="card">
        <div class="card-header">
            <h5 v-text="theCardTitle"></h5>
            <button @click="sendMessage" class="btn">给子组件发送一个消息</button>
        </div>
        <div class="card-body">
            <child-card :parentMessage="parentMessage" @finished="finished" :ok="ok"></child-card>
        </div>
    </div>
</template>

<script>
    import ChildCard from "./ChildCard";

    export default {
        name: "ParentCard",
        data: () => ({
            theCardTitle: "父组件",
            parentMessage: ""
        }),
        components: {
            ChildCard
        },
        methods: {
            sendMessage() {
                this.parentMessage = `<b>消息来自父组件:</b> (^_^)!!!`;
            },
            finished() {
                this.parentMessage = ''
            },
            ok() {
                this.finished()
            }
        }
    };
</script>

现在我们要做的是更新子组件上的props。这样做的目的是通过props将回调函数从父组件中传递到子组件。我们可以像下面这样做:

<!-- ChildCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text" v-html="theCardBody"></p>
            <div v-if="parentMessage" class="alert" v-html="parentMessage"></div>
            <button v-if="parentMessage" @click="ok" class="btn">OK</button>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'ChildCard',
        props: ['parentMessage', 'ok'],
        data: () => ({
            theCardTitle: '子组件',
            theCardBody: '我是一个子组件!(^_^) !!!'
        })
    }
</script>

父组件和子组件之间的交互与上面的示例是相同的。

你可以使用任何对你来说比较容易的方案,但是在从子组件到父组件的通讯中,发出自定义事件似乎是较为更流行的一种。

现在我们知道如何通过props实现父组件向子组件之间的通讯以及如何通过自定义事件完成子组件向父组件之间的通讯。除了这两种之外,还有另外一种情形,那就是兄弟之间的组件如何进行数据通讯。那么接下来,咱们就来学习这方面的知识。

兄弟组件通讯

在Vue中实现兄弟组件的通讯也有几种方法,其中一种方法是让父组件允当两个子组件之间的中间件(中继);另一种就是使用EventBus(事件总线),它允许两个子组件之间直接通讯,而不需要涉及父组件。

通过父组件进行兄弟组件之间通讯

先来看第一个方法,就是让兄弟组件通过一个共同的父组件彼此通讯

我们还是通过示例来学习。接下来的这个示例包含父组件和两个子组件,这两个子组件是兄弟组件。单击兄弟组件上的按钮,可以看到他们之间可以相互通讯。

首先创建ParentCard组件:

<!-- ParentCard.vue -->
<template>
    <div class="card">
        <div class="card-header">
            <h5 v-text="theCardTitle"></h5>
            <button @click="momSaidChill" v-if="stopFighting()" class="btn">停止通讯</button>
        </div>
        <div class="card-body">
            <brother-card :messageSon="messageson" @brotherSaid="messageDaughter($event)"></brother-card>
            <sister-card :messageDaughter="messagedaughter" @sisterSaid="messageSon($event)"></sister-card>
        </div>
    </div>
</template>

<script>
    import BrotherCard from './BrotherCard';
    import SisterCard from './SisterCard'

    export default {
        name: 'ParentCard',
        data: () => ({
            theCardTitle: '父组件',
            messagedaughter:'', 
            messageson:''
        }),
        components: {
            BrotherCard,
            SisterCard
        },
        methods: {
            messageDaughter(message) {
                this.messagedaughter = message;
            },
            messageSon(message) {
                this.messageson = message;
            },
            stopFighting() {
                if (this.messagedaughter && this.messageson) {
                    return true
                }
                return false
            },
            momSaidChill() {
                this.messagedaughter = '',
                this.messageson = ''
            }
        }
    };
</script>

创建SisterCard组件:

<!-- SisterCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text">我是Sister组件</p>
            <button @click="messageBrother" class="btn">给哥哥发消息</button>
            <div v-if="messageDaughter" class="alert" v-html="messageDaughter"></div>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'SisterCard',
        props: ['messageDaughter'],
        data: () => ({
            theCardTitle: '子组件2'
        }),
        methods: {
            messageBrother() {
                this.$emit('sisterSaid', '妈妈说,该做作业了!(^_^)!!!')
            }
        }
    }
</script>

接着创建BrotherCard组件:

<!-- BrotherCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text">我是Brother组件</p>
            <button @click="messageSister" class="btn">给妹妹发消息</button>

            <div v-if="messageSon" class="alert" v-html="messageSon"></div>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'BrotherCard',
        props: ['messageSon'],
        data: () => ({
            theCardTitle: '子组件1'
        }),
        methods: {
            messageSister() {
                this.$emit('brotherSaid', '妈妈说,该做作业了!(^_^)!!!')
            }
        }
    }
</script>

最终效果如下:

接下来简单看看这个实现过程。

SisterCard通过ParentCardBrotherCard通讯

首先来看SisterCard是如何与BrotherCard通讯的。从示例中可以看出,他们两之间的通讯是通过其父组件ParentCard作为中间媒介来进行通讯的。

我们在SisterCard组件的<template>中为messageBrother()方法设置了一个@click事件来监听该事件。

<button @click="messageBrother" class="btn">给哥哥发消息</button>

当用户点击SisterCard中的“给哥哥发消息”将会触发messageBrother()方法。在这个方法中,将发出一个sisterSaid事件,并且把妈妈说,该做作业了!(^_^)!!!信息发送出去。

methods: {
    messageBrother() {
        this.$emit("sisterSaid", "妈妈说,该做作业了!(^_^)!!!");
    }
}

ParentCard<template>中定制了一个@sisterSaid事件侦听器,它触发了messageSon()方法。所以父组件在这两个兄弟组件之间起到了传递的作用。

<sister-card :messageDaughter="messagedaughter" @sisterSaid="messageSon($event)"></sister-card>

另外在ParentCard组件中声明了messageSon()方法,该方法接受上面发送的自定义事件,并将其设置为messageson属性。

messageSon(message) {
    this.messageson = message;
},

这样一来,ParentCard组件中messageson就由空字符串变成了妈妈说,该做作业了!(^_^)!!!

接着在ParentCard组件自定义标签<brother-card>通过:messageSon="messageson"的方式将messageson属性绑定到<brother-card>

<brother-card :messageSon="messageson" @brotherSaid="messageDaughter($event)"></brother-card>

这个时候在BrotherCard组件中设置props的属性值为messageSon。这样就可以访问源自于SisterCard组件的数据,并且该数据在BrotherCard中显示。

props: ["messageSon"],

最后在BrotherCard组件就可以使用该数据。我们可以通过v-if指令来查看messageSon是否有任何有用的数据,如果有,那么就在div.alert中显示该消息:

<div v-if="messageSon" class="alert" v-html="messageSon"></div>

上面的描述过程也适用于BrotherCard通过ParentCardSisterCard进行数据通讯。

通过EventBus进行兄弟间组件通讯

随着应用程序越来越庞大,通过父组件来传递所有内容会把事情变得越来越棘手。不过我们还有另一种选择,那就是使用EventBus架起兄弟之间通讯的桥梁。接下来看看我们是如何利用这一点一完成兄弟组件之间的数据通讯。

我们同样基于上面的示例来做修改。接下来的示例中,ParentCard组件包含了SisterCardBrotherCard两个子组件,而且这两个子组件是兄弟组件。

首先在main.js文件中定义一个新的eventBus对象,其实他是一个全新的Vue实例:

// main.js
import Vue from 'vue'
import App from './App'

export const eventBus = new Vue()

new Vue({
    el: '#app',
    render: h => h(App)
})

接着在新创建的BrotherCard组件导入main.js

<!-- BrotherCard.vue -->

<script>
    import { eventBus } from '../main'
</script>

eventBus实例现在将成为BrotherCard组件中发出事件的实例。现在我们可以使用eventBus.$emit来替代上例中的this.$emiteventBus是一个Vue实例,而且eventBus有这个$emit方法,这就是我们能够这么用的原因。这样做同样会触发相同的自定义事件名称和消息。

methods: {
    messageSister() {
        eventBus.$emit('brotherSaid', '妈妈说,该做作业了!(^_^)!!!')
    }
}

同样可以在SisterCard组件中引入eventBus

<script>
    import { eventBus } from '../main'
</script>

created()生命周期钩子添加到SisterCard组件。在created()钩子中添加eventBus启动自定义事件的侦听器。当使用SisterCard组件时,该侦听器将开始运行并且会保持运行。下面的代码只是侦听brotherSaid自定义事件,然后触发回调,将作为自定义事件有效负载传递的消息分配给fromBrother

created() {
    eventBus.$on('brotherSaid', (message) => {
        this.fromBrother = message
    })
}

这样就可以有条件地显示来自BrotherCard的信息:

<div v-if="fromBrother" class="alert" v-html="fromBrother"></div>

上面看到的是如何通过eventBus实现SisterCardBrotherCard传递数据的方式,反之,BrotherCard向SisterCard`传递数据也可以使用类似的方式。

最终代码如下:

<!-- SisterCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text">我是Sister组件</p>
            <button @click="messageBrother" class="btn">给哥哥发消息</button>

            <div v-if="fromBrother" class="alert" v-html="fromBrother"></div>
        </div>
    </div>
</template>

<script>
    import { eventBus } from "../main";

    export default {
        name: "SisterCard",
        data: () => ({
            theCardTitle: "Sister Card",
            fromBrother: ""
        }),
        methods: {
            messageBrother() {
                eventBus.$emit("sisterSaid", "妈妈说,该做作业了!(^_^)!!!");
            }
        },
        created() {
            eventBus.$on("brotherSaid", message => {
                this.fromBrother = message;
            });
        }
    };
</script>

<!-- BrotherCard.vue -->
<template>
    <div class="message">
        <div class="message-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="message-body">
            <p class="message-text">我是Brother组件</p>
            <button @click="messageSister" class="btn">给妹妹发消息</button>

            <div v-if="fromSister" class="alert" v-html="fromSister"></div>
        </div>
    </div>
</template>

<script>
    import { eventBus } from "../main.js";

    export default {
        name: "BrotherCard",
        data: () => ({
            theCardTitle: "Brother Card",
            fromSister: ""
        }),
        methods: {
            messageSister() {
                eventBus.$emit("brotherSaid", "妈妈说,该做作业了!(^_^)!!!");
            }
        },
        created() {
            eventBus.$on("sisterSaid", message => {
                this.fromSister = message;
            });
        }
    };
</script>

最后创建的ParentCard组件,我们可以像下面这样编码:

<!-- ParentCard -->
<template>
    <div class="card">
        <div class="card-header">
            <h5 v-text="theCardTitle"></h5>
        </div>
        <div class="card-body">
            <brother-card></brother-card>
            <sister-card></sister-card>
        </div>
    </div>
</template>

<script>
    import BrotherCard from "./BrotherCard";
    import SisterCard from "./SisterCard";

    export default {
        name: "ParentCard",
        data: () => ({
            theCardTitle: "Parent Card"
        }),
        components: {
            BrotherCard,
            SisterCard
        }
    };
</script>

最终看到的效果如下:

总结

在本教程中,我们学习了在Vue中如何实现组件之间的通讯。通过实例看到了如何实现父组件向子组件,子组件向父组件以及兄弟组件间的数据通讯。简单的根据为:

  • 通过props可以实现父组件向子组件发送数据
  • 通过自定义事件可以实现子组件向父组件发送数据
  • 兄弟组件数据通讯除了借助共同的父组件做为通讯桥梁之外,还可以通过eventBus来让兄弟之间组件进行数据通讯

最后用一张图来简单的描述一下:

参考阅读

  • VueJs Parent Child Communication
  • Vue Sibling Component CommunicationNike React Element 87 Moss