前言
在本人最近的面试中,关于vue之间的组件通信的方式是经常被面试官提起,随着面试场次的增多,本人对于组件通信方式的理解 从只会 'props'、'emit'和vuex
之外学到了更多的方式。
为什么要有组件通信
我们都知道,在vue中当我们需要将当前页面组件的信息传递给另一个组件的时候,就需要实现vue的组件通信功能。因为Vue是一个组件化的框架,应用程序通常被拆分成许多小组件,每个组件负责一个特定的功能。组件通信允许这些组件相互交流和协作,使得构建复杂的应用程序变得更加容易和灵活。
因此组件通信在Vue中是必需的,因为它使得构建复杂的应用程序变得更加容易和灵活,具体原因包括:
- 组件化开发:Vue是一个组件化的框架,应用程序通常被拆分成许多小组件,每个组件负责一个特定的功能。组件通信使得这些组件能够相互交流和协作,从而构建出功能丰富的应用程序。
- 解耦和复用:通过组件通信,可以将应用程序拆分成独立的组件,每个组件只关注自己的功能,不需要关心其他组件的实现细节。这样可以降低组件之间的耦合度,提高组件的复用性和可维护性。
- 状态管理:组件通信使得状态管理变得更加简单和灵活。通过向子组件传递props和监听子组件的事件,可以实现父子组件之间的数据传递和状态管理。而通过全局事件总线、Vuex等工具,可以实现任意组件之间的状态共享和管理。
- 用户交互:在用户交互方面,组件通信可以实现组件之间的消息传递和事件触发。例如,一个按钮组件可以触发一个点击事件,通知其他组件执行相应的操作。
- 跨组件通信:有时候需要在不相邻的组件之间进行通信,例如兄弟组件之间或者跨级组件之间。通过提供/注入、全局事件总线、Vuex等方式,可以实现跨组件的数据传递和状态管理。
组件通信的方式
那么都有哪些方式可以实现组件通信呢?本人在最近的学习中共学习到了以下的实现方式:
- 父子组件通信:父组件通过props向子组件传递数据,子组件通过defineProps接收数据
- 父子组件通信:父组件provide,子组件inject
- 子父组件通信:子组件拿到父组件的数据并修改后emit出来,父组件通过v-model实现双向绑定
- 子父组件通信:子组件defineExpose暴露方法,父组件通过ref读取整个子组件对象来获取值
- 子父组件通信:子组件通过emit触发事件,父组件通过v-on(@) 监听事件
- EventBus 事件总线 vue3中不推荐使用,但是可以通过 mitt 插件实现
- vuex
下面本人将为大家讲解一下如何用以上方式来实现组件通信
父子组件通信
首先就是父子组件通信方式的实现了。
props
如以上代码
vue
// 父组件
<template>
<div class="hd">
<input type="text" v-model="msg">
<button @click="add">添加</button>
</div>
<Child :list="list"/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/Child_1.vue'
const list = ref(['html', 'css'])
const msg = ref('')
const add = () => {
list.value.push(msg.value)
msg.value = ''
}
</script>
<style lang="css" scoped>
.hd input{
width: 200px;
padding: 10px 0;
font-size: 10px;
}
.hd button{
padding: 10px 20px;
font-size: 20px;
}
</style>
vue
//子组件
<template>
<div class="child">
<div class="bd">
<ul>
<li v-for="item in list" :key="item">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
list: {
type: Array,
default: () => []
}
})
</script>
<style scoped>
.child {
margin-top: 20px;
}
.bd {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
}
</style>
以上代码中,在父组件中,我们通过<Child :list="list"/>
语法,将父组件的list
数据传递给子组件。并且使用 ref
创建了名为 list
、msg
和 add
的响应式数据。list
用于存储列表项,msg
用于存储输入框的值,add
函数用于向 list
数组中添加新的列表项。
子组件中,通过defineProps
定义了一个list
属性,其类型为数组,默认值为空数组。然后在模板中使用了v-for
指令遍历list
数组,将数组中的每个元素渲染成一个列表项。
在父组件中,通过将子组件Child
的list
属性绑定到父组件中的msg
变量,实现了父组件向子组件传递数据的功能。当父组件中的msg
变量发生变化时,子组件中的list
属性也会相应地更新。
provide和inject
在vue中,我们可以通过导入provide
和inject
,开实现父子组件之间的通信。
特点
- 灵活性:
provide
和inject
不受组件层次结构的限制,可以在任何深度的组件中使用,从而使得数据传递更加灵活。 - 跨层级通信: 与 props/downward data flow 不同,
provide
和inject
可以实现跨层级的组件通信,无需一层层地传递 props,从而简化了组件之间的通信。 - 依赖注入的思想: 类似于依赖注入的思想,使得组件之间的依赖关系更加清晰,降低了组件之间的耦合度。
vue
<template>
<div class="hd">
<input type="text" v-model="msg">
<button @click="add">添加</button>
</div>
<!-- <Child :list="list"/> -->
<Child />
</template>
<script setup>
import { ref,provide } from 'vue'
import Child from './components/Child_5.vue'
const msg = ref('')
const list = ref(['html', 'css'])
provide('list', list.value);
const add = () => {
list.value.push(msg.value)
msg.value = ''
}
</script>
<style lang="css" scoped>
同上
</style>
vue
<template>
<div class="bd">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { ref,inject } from 'vue'
const list = inject('list')
</script>
<style lang="css" scoped>
</style>
在以上代码中,我们通过父组件 provide
提供了 list
数据,而子组件则通过 inject
获取了这个数据,从而实现了组件之间的通信。这种方式可以在组件层次结构中的任何深度进行通信,而不需要一层层地传递 props,因此更加灵活方便。
但是需要注意的是它只能由祖先组件向子孙组件发送数据。并且不太推荐使用这种方式来实现父子组件之间的通信,因为它与我们所认知的单向数据流的思想相悖。
子父组件通信
emit
vue
<template>
<Child @add="handle"/>
<div class="bd">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './components/Child_2.vue'
const list = ref(['html', 'css'])
const handle = (e) => {
list.value.push(e)
}
</script>
vue
<template>
<div class="hd">
<input type="text" name="" id="" v-model="msg">
<button @click="add">添加</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('')
const emits = defineEmits(['add']) //创建一个add事件
const add = () => {
emits('add', msg.value);
console.log(msg.value);
msg.value = '';
}
</script>
在以上代码中,通过 defineEmits
函数来定义了一个名为 add
的自定义事件。这个函数的作用是创建一个函数,用于触发组件的自定义事件。
具体来说,在子组件中,我们首先使用 defineEmits
函数来创建了一个名为 emits
的对象,该对象包含了定义的自定义事件。然后,在子组件的 add
方法中,我们通过 emits
对象来触发了 add
事件,并且传递了 msg.value
作为参数。
这样,在父组件中,我们通过监听子组件触发的 add
事件,并在事件处理函数中接收到传递过来的参数,从而实现了子组件向父组件的通信。
v-model
我们都知道v-model
在 Vue.js 中通常用于实现表单元素和组件之间的双向数据绑定,但它也可以用于自定义组件之间的通信。这是因为 v-model
实际上是一个语法糖,它背后的原理是通过将 value
属性和 input
事件结合起来实现数据的双向绑定。
当你在一个自定义组件上使用 v-model
时,Vue.js 实际上会做两件事情:
- 将
value
属性传递给组件作为 props。 - 监听组件内部触发的
input
事件,并根据该事件更新外部数据。
因此,通过在自定义组件内部正确地处理 value
属性和触发 input
事件,我们可以使自定义组件支持 v-model
语法,并实现与父组件之间的双向数据绑定。
vue
<template>
<Child v-model:list="list"/>
<div class="bd">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './components/Child_3.vue'
const list = ref(['html', 'css'])
</script>
vue
<template>
<div class="hd">
<input type="text" name="" id="" v-model="msg">
<button @click="add">添加</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('')
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['update:list']) // 事件名必须是 update:xxx
const add = () => {
const arr = props.list
arr.push(msg.value)
// 不要写这种代码:props.list.push(msg.value)
emits('update:list', arr)
msg.value = ''
}
</script>
以上代码中,我们在父组件中,通过 <Child v-model:list="list"/>
这样的语法,将 list
数据作为 v-model
的值传递给了子组件 Child
。这样做的效果是,父组件的 list
数据会被传递给子组件,并且在子组件内部使用 v-model
时,list
数据将作为 value
属性的值。
在子组件 Child
中,使用了 v-model="msg"
,这样做的效果是,msg
数据将与输入框进行双向绑定。也就是说,当输入框中的值发生变化时,msg
数据也会跟着变化;反之亦然。
在子组件中,通过点击按钮触发 add
方法,将输入框中的值添加到 list
数组中。同时,通过 emits('update:list', arr)
将更新后的 list
数组发送给父组件。
在父组件中,由于使用了 v-model
,因此子组件发送的 list
数据更新会立即反映在父组件中,从而实现了双向数据绑定和组件通信的效果。
defineExpose
实际上defineExpose
并不是专门用于实现组件通信的功能,而是 Vue 3 中的一个用于暴露组件内部数据或方法给父组件的 API。在 Vue 3 中,组件的内部数据和方法默认是私有的,只能在组件内部访问,而无法直接通过组件实例访问。但是,有时候我们希望将某些数据或方法暴露给父组件,以便父组件可以直接访问或操作。
defineExpose
的作用就是将组件内部的数据或方法暴露给父组件。当组件使用 defineExpose
声明后,父组件就可以通过组件实例直接访问这些暴露的数据或方法,从而实现了组件之间的通信。
vue
<template>
<Child ref="childRef"/>
<div class="bd">
<ul>
<li v-for="item in childRef?.list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './components/Child_4.vue'
const childRef = ref(null)
</script>
vue
<template>
<div class="hd">
<input type="text" name="" id="" v-model="msg">
<button @click="add">添加</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const list = ref(['html', 'css'])
const msg = ref('')
const add = () => {
list.value.push(msg.value)
msg.value = ''
}
defineExpose({list}) //子组件暴露出去的数据
</script>
以上代码中,我们在子组件中通过 defineExpose
来暴露子组件的 list
数据,这样父组件就可以直接通过 childRef.list
访问子组件的 list
数据了
兄弟组件通信
EventBus 事件总线
事件总线是一种常见的 Vue 应用程序中组件通信的方式,它通过一个中央事件总线来实现组件之间的通信,允许不同组件之间进行解耦合的通信。
vue
//App.vue
<template>
<div>
<ChildA />
<ChildB />
</div>
</template>
<script setup>
import ChildA from './components/child6/A.vue'
import ChildB from './components/child6/B.vue'
</script>
vue
//A.vue
<template>
<div>
<input type="text" v-model="msg" />
<button @click="add">添加</button>
</div>
</template>
<script setup>
import emitter from './emit.js'
import { onMounted, ref } from "vue";
const list = ref(["html", "css"]);
onMounted(() => {
emitter.emit("list", list.value);
});
const msg = ref("");
const add = () => {
list.value.push(msg.value);
msg.value = "";
};
vue
//B.vue
<template>
<div>
<ul>
<li v-for="item in list ">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import emitter from './emit';
import { ref } from 'vue';
const list = ref([])
emitter.on('list', (e) => {
console.log(e);
list.value = e;
})
</script>
js
// emit.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
首先,因为在vue3中不支持直接使用Eventbus
,我们需要通过import mitt from 'mitt'
来创建事件总线。需要注意的是我们需要去下载这个mitt
插件。
在 ChildA
组件中,当组件挂载完成时,我们通过事件总线向外发送了一个名为 list
的事件,并携带了自己的 list
数据。这里的 emitter.emit("list", list.value)
表示发送一个名为 list
的事件,并将当前 list
数据作为参数传递给这个事件。
在 ChildB
组件中,它监听了事件总线上的 list
事件,并在事件被触发时执行相应的回调函数,更新自己的数据。这里的 emitter.on('list', callback)
表示监听名为 list
的事件,并在事件被触发时执行 callback
回调函数。在回调函数中,它将收到的数据更新到 ChildB
组件的 list
中。
的来说,通过事件总线,ChildA
组件发送事件并携带数据,而 ChildB
组件监听这个事件,并在事件被触发时接收数据,从而实现了组件之间的通信。
vuex
在vue中我们可以通过vuex来实现组件之间的通信。因为Vuex 是 Vue.js 官方提供的状态管理库,它可以帮助我们集中管理应用中的状态,并提供了一种在组件之间共享状态的机制。
-
安装 Vuex: 首先,我们需要去安装 Vuex。
bashnpm install vuex # 或者 yarn add vuex
-
创建 Vuex Store: 在文件夹中,我们需要创建一个 store 文件来管理应用的状态。在 Vuex store 中,我们可以定义状态、mutations、actions 等。
js// store/index.js import { createStore } from 'vuex'; const store = createStore({ state: { list: ['html', 'css'], }, mutations: { updateList(state, newList) { state.list = newList; }, }, actions: { updateList(context, newList) { context.commit('updateList', newList); }, }, getters: { getList(state) { return state.list; }, }, }); export default store;
-
使用 Vuex: 将创建的 Vuex store 导入到 main.js 中,以便所有组件都能够访问到 Vuex 中的状态和方法。
js// main.js import { createApp } from 'vue'; import App from './App.vue'; import store from './store'; const app = createApp(App); app.use(store); app.mount('#app');
-
在兄弟组件中使用 Vuex:
-
ChildA 组件:在需要更新状态的地方,通过调用 Vuex store 中的 action 来更新状态。
javascript// ChildA.vue <template> <button @click="updateList">Update List</button> </template> <script> import { mapActions } from 'vuex'; export default { methods: { ...mapActions(['updateList']), }, }; </script>
-
ChildB 组件:通过计算属性(computed property)或者直接访问 Vuex store 的 getters 来获取状态。
javascript// ChildB.vue <template> <div> <ul> <li v-for="item in list" :key="item">{{ item }}</li> </ul> </div> </template> <script> import { mapGetters } from 'vuex'; export default { computed: { ...mapGetters(['getList']), list() { return this.getList; }, }, }; </script>
-
通过以上步骤,我们就可以在兄弟组件之间实现通信了。当 ChildA
组件触发更新时,通过 Vuex store 中的 action 更新状态,然后 ChildB
组件通过计算属性或者直接访问 getters 来获取更新后的状态,从而实现了兄弟组件之间的通信。
总结
我所了解的组件之间的通信方式就是以上的所有内容了,如有不足之处,欢迎大家加以补充。