前言
当组件间通信的逻辑较为简单时,使用 Prop 和自定义事件足以应对;
但是当出现全局共享的状态、兄弟组件间通信等场景时,使用 Prop 和自定义事件可能会让逻辑变得非常复杂。这个时候,你应该考虑使用一个全局状态管理方案,例如 Vuex。
当然,Vue 组件间通信的通信方式远不止以上两种,下面分别讨论。
一、props down, events up
通常情况下,子组件不处理业务逻辑,只向上派发事件,所以,父子组件间经常需要进行数据传递。
在 Vue 中,父子组件的关系可以总结为 props down, events up
,基本流程是:
- 在父组件中,通过 Prop 向子组件传递数据
- 在子组件中,通过触发(
emit
)一个自定义事件,然后在父组件中使用v-on
进行监听
下面是一个使用 Prop 方式通信的一个 TodoList 示例:
<!-- 父组件 Todo.vue -->
<template>
<ul class="todo-list">
<TodoItem v-for="item in todos" :key="item.id" :item="item" @remove="removeItem" />
</ul>
</template>
<script>
import TodoItem from './components/TodoItem.vue'
export default {
components: {
TodoItem,
},
data() {
return {
todos: [
{ id: 1, text: 'Learning Vue' },
{ id: 2, text: 'Learning React' },
{ id: 3, text: 'Learning Node.js' },
],
}
},
methods: {
removeItem(id) {
this.todos = this.todos.filter((i) => i.id !== id)
},
},
}
</script>
<!-- 子组件 TodoItem.vue -->
<template>
<li @click="remove">{{ item.text }}</li>
</template>
<script>
export default {
props: ['item'],
methods: {
remove() {
this.$emit('remove', this.item.id)
},
},
}
</script>
二、Vuex
Prop 和自定义事件这种“单向数据流”的方式只适合用来解决简单场景下的组件间通信,当出现下列场景时,使用 Prop 处理起来可能非常棘手:
- 兄弟组件间的状态传递
- 多层嵌套组件间的状态传递
- 全局共享的状态
当出现上述场景时,你应该使用一个全局状态管理方案,例如 Vuex。
Vuex 是一个专为 Vue.js 应用程序开发的集中式状态管理库。核心概念是:
- State:即 Vuex 维护的全局状态,它是一颗单一状态树(用一个对象包含了全部的状态),并且这些状态是响应式的。
- Mutation:即同步事务,改变 store 中的状态的唯一途径就是显式地提交 mutation,这样使得我们可以方便地跟踪每一个状态的变化。
- Action:即异步事务,Action 通过提交的 mutation 来间接地改变状态,而不是直接变更状态(State)
三、vm.$root
我们可以将全局状态挂载到 Vue 根实例上,如下所示:
// main.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: (h) => h(App),
data: {
theme: 'light',
},
methods: {
setTheme(newValue) {
console.log('setTheme triggered with', newValue)
this.theme = newValue
},
},
})
然后,你就可以在任意子组件中通过 this.$root
来调用根实例上的属性和方法,如:this.$root.theme
、this.$root.setTheme()
,你甚至可以直接在子组件中对根实例上面的属性重新赋值,如 this.$root.theme = 'dark'
于是乎,这个根实例就成为了一个全局 store。
这很方便,但这也带来了一些问题,比如你不能很方便地跟踪每一个状态的变化,你也不能很好地将 store 划分为多个模块。
所以,对于非常小型的应用,使用这种方式确实很方便;但是在中大型应用中,强烈推荐使用 Vuex 来管理应用的状态。
我们应该避免在应用中使用
vm.$root
/vm.$parent
/vm.$children
,因为这种强关联背离了组件的解耦原则,也会让状态的流向变得不可控。
四、provide / inject
当我们需要向更深层级的组件传递信息时,使用 Prop 和 $parent
都不太方便。
Vue 提供了一种能力,让我们可以向任意更深层级的组件提供上下文信息 —— provide / inject
,它被称为“依赖注入”,你可以理解为“大范围有效的 prop”。
provide
选项允许我们指定我们想要提供给后代组件的数据/方法- 然后在任何后代组件里,我们都可以使用
inject
选项来接收指定的我们想要添加在这个实例上的 property
// 祖先组件
export default {
provide() {
return {
theme: 'light',
}
},
}
// 后代组件
export default {
inject: ['theme'],
mounted() {
console.log(this.theme)
},
}
然而,依赖注入还是有负面影响的:
- 它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。
- 同时所提供的 property 并不是响应式的。
provide 和 inject 主要在开发高阶插件/组件库时使用,并不推荐用于普通应用程序代码中。
五、Event Bus
Event Bus 又叫“全局事件总线”,是一个简易的全局状态管理器。
具体的做法是:实例化一个空的 Vue 实例来作为事件中心,然后在组件中,可以使用 $emit
、$on
、$off
分别来分发、监听、取消监听事件
// event-bus.js
import Vue from 'vue'
const EventBus = new Vue({
data: {
count: 0,
},
created() {
// 监听自定义事件, 事件可以由 vm.$emit 触发
this.$on('increase', this.increase)
},
// 最好在组件销毁前, 清除事件监听
beforeDestroy: function () {
this.$off('increase', this.increase)
},
methods: {
increase(count) {
this.count += count
},
},
})
export default EventBus
然后,你可以在组件中这样使用:
<template>
<div>
<p>{{ count }}</p>
<button @click="increase(10)">Increase</button>
</div>
</template>
<script>
import EventBus from '../event-bus'
export default {
computed: {
count() {
return EventBus.count
},
},
methods: {
increase(count) {
EventBus.$emit('increase', count)
},
},
}
</script>
六、Vue.observable(object)
Vue.observable(object)
是 2.6.0 新增的一个全局 API,用于让一个对象变成响应式的,Vue 内部会用它来处理 data
函数返回的对象。
与 Event Bus 类似,我们也可以使用 Vue.observable(object)
来作为最小化的全局状态管理器,用于简单的场景:
// store.js
import Vue from 'vue'
// 模拟 Vuex Store
export const store = Vue.observable({
count: 0,
})
// 模拟 Vuex mutations
export const mutations = {
increase(count) {
store.count += count
},
}
然后,你可以在组件中这样使用:
<template>
<div>
<p>{{ count }}</p>
<button @click="increase(10)">Increase</button>
</div>
</template>
<script>
import { store, mutations } from '/path/to/store'
export default {
computed: {
count() {
return store.count
},
},
methods: {
increase: mutations.increase,
},
}
</script>
后记
如果文中出现纰漏,或者您有其他的更好的解决方案,欢迎留言讨论 🐤
参考资料: