在 Vue.js 中,组件之间的通信是构建复杂应用的基础。本文将以一个简易的TodoList为例,将输入待办事项的输入框和记录事件的列表分为两个组件,介绍几种常见的组件通信方式,帮助你更好地理解如何在父子组件之间传递数据和事件。
1. 父子通信:通过 props 传值
demo1:基本使用
在父组件中,我们可以通过 props 向子组件传递数据。子组件使用 defineProps 接收父组件传递的这些数据,可以分别使用type和default定义接收数据的类型和默认值。
父组件
<template>
<div>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child :list="list"></Child>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const newMsg = ref('');
const list = ref(['html', 'css']);
const add = () => {
list.value.push(newMsg.value);
newMsg.value = '';
};
</script>
子组件
<template>
<div>
<div class="body">
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
const props = defineProps({
list: {
type: Array,//接收值的类型
default: () => []//如果未接收到任何值时的默认值
}
});
</script>
demo2:通过响应式数据
在demo1中,我们是在父组件中完成了将输入的待办事项添加到列表中,子组件只负责接收数据以及显示数据。如果我们要在子组件中完成添加数据的操作呢?我们可以在子组件中使用 onBeforeUpdate 生命周期钩子监听 props 的变化,并将其添加到内部列表中。
父组件
<template>
<div>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child :msg = "val" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const newMsg = ref('')
const val = ref('')
const add = () => {
val.value = newMsg.value
}
</script>
<style lang="css" scoped>
</style>
子组件
<template>
<div>
<div class="body">
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeUpdate } from 'vue';
const list = ref([]);
const props = defineProps({
msg: ''
});
onBeforeUpdate(() => {
list.value.push(props.msg);
});
</script>
在demo2中父组件只负责将用户输入的待办事项传递给子组件,而子组件通过注册一个onBeforeUpdate 生命周期钩子,在组件即将因为响应式状态msg变更而更新其 DOM 树之前调用回调函数(通过defineProps返回的对象包含了所有传递给该组件的prop,并且这些prop默认是响应式的),将父组件传递过来的值添加到列表中。
2. 子父通信:通过发布订阅机制
demo3:事件发布与订阅
在这种方式中,子组件可以发布一个事件,父组件则订阅这个事件,从而接收子组件传递的数据。
子组件
<template>
<div>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const newMsg = ref('');
const emits = defineEmits(['addMsg']); // 声明事件
const add = () => {
emits('addMsg', newMsg.value); // 发布事件
};
</script>
父组件
<template>
<div>
<Child @addMsg="handle"></Child>
<div class="body">
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const list = ref(['html', 'css']);
const handle = (msg) => {
list.value.push(msg);
};
</script>
每当用户输入待办事项并点击确定时,都会触发add函数,即发布一个事件addMsg并把要传递的数据一起发布出去,此时订阅了该事件的父组件的handle函数也会触发并用形参接收到了发布出来的数据。
3. 使用 v-model 实现双向绑定
demo4:v-model 绑定
通过 v-model 指令,可以实现父组件与子组件之间的双向数据绑定。
子组件
<template>
<div>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
list: Array
});
const newMsg = ref('');
const emits = defineEmits(['update:list']);
const add = () => {
const arr = props.list
arr.push(newMsg.value)
emits('update:list',arr) // 发布更新事件
};
</script>
父组件
<template>
<div>
<Child v-model:list="list" />
<div class="body">
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const list = ref(['html', 'css']);
</script>
在这个栗子🌰中,父组件通过v-model绑定了传递给子组件的数据,子组件通过defineProps接收到数据后进行一些必要的操作,然后通过发布update:xxx事件和操作后的数据通知父组件数据更新了。
这时候可能就有的小伙伴要问了,既然实现了父子组件的双向数据绑定为什么不能直接props.list.push(newMsg.value)修改数据呢,试想如果有多个子组件同时修改父组件的同一个值的话那不乱套了,父组件不知道谁对数据进行了什么操作,更重要的是vue官方也不推荐我们这样做。
4. 父组件通过 ref 获取子组件的 DOM 结构
demo5:使用 defineExpose
父组件可以通过 ref 获取子组件实例,并访问其通过defineExpose()暴露的方法和属性。
子组件
<template>
<div>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const newMsg = ref('');
const list = ref(['html', 'css']);
const add = () => {
list.value.push(newMsg.value);
};
defineExpose({ list }); // 暴露响应式属性
</script>
父组件
<template>
<div>
<Child ref="childRef" />
<div class="body">
<ul>
<li v-for="(item, index) in childRef?.list" :key="index">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
const childRef = ref(null);
onMounted(() => {
console.log(childRef.value.list); // 访问子组件的 list 属性
});
</script>
大部分人都知道ref可以创建响应式数据但并不知道它还可以用来获取DOM元素,我们首先要在setup函数中定义一个用来标记DOM元素的ref对象,然后将这个 ref 绑定到你想要操作的元素或组件上,确保在 mounted 钩子或之后再访问 DOM 元素,因为只有当组件挂载完成后,DOM 才会被创建出来。我们这里使用了可选链操作符 ?.这样即使 childRef 为 null 或 undefined,也不会导致运行时错误。
注意:使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露
5. 使用 provide 和 inject 实现全局属性传递
demo6:全局数据共享
通过 provide 父组件可以向全局提供数据或方法,而需要用到的地方可以通过inject随时获取。
子组件
<template>
<div>
<div class="body">
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue';
const list = inject('list'); // 注入全局列表
</script>
父组件
<template>
<div>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child />
</div>
</template>
<script setup>
import { provide, ref } from 'vue';
import Child from './Child.vue';
const newMsg = ref('');
const list = ref(['html', 'css']);
const add = () => {
list.value.push(newMsg.value);
};
provide('list', list); // 提供全局列表
</script>
在这个栗子中父组件通过provide()向全局提供了数据list并取了一个键名,子组件通过inject()和该键名即可获取父组件提供的数据。
注意:如果多个组件使用 provide 提供了相同键名的数据,那么子组件通过 inject 接收到的数据将是离它最近的祖先组件所提供的值,即在组件树中的层级上最接近的那个提供者。
结语
以上就是vue常用的组件通信方式,如有遗漏欢迎jym在评论区补充。通过这些组件通信方式,你可以在 Vue 应用中灵活地管理数据流动和事件响应。这将使你的应用更加模块化和易于维护。希望本文能帮助你更好地理解 Vue 组件之间的通信方式!