在现代前端开发中,Vue.js 是一种非常流行的选择。Vue 提供了一套简洁、灵活且强大的 API,使得开发者能够高效地构建复杂的应用程序。在 Vue 应用中,组件是基本的构建单元,而组件间的通信则是应用架构设计的关键部分之一。在最近的学习中,已经初步的在了解一个完整的项目的开发,发现存在很多情况都涉及到了组件之间的传参问题,也了解到很多可以现实的方法,在本文中我将做一个系统性的归纳总结。
父子通信
现在来一个场景,实现一个简单的todolist添加效果,要求将list做成子组件实现展示list。
1. 父组件传值给子组件
这是最常见也是最直接的通信方式,父组件可以通过属性(props)向子组件传递数据。
使用props有两种方法可以实现(这两个方法大差不差方法类似):
- 我们可以在父组件中对list数组进行添加处理后,再将数组传给list组件渲染。
- 将添加的list传给list组件,让子组件自己对list数组进行管理添加后再渲染。
// 父组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child :list="list" />
</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)
}
</script>
<style lang="css" scoped></style>
// 子组件
<template>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
list: Array,//传进来参数的数据类型
default: () => []//默认值
})
</script>
<style lang="css" scoped></style>
2. 使用 provide/inject
这种方式允许在组件树中无限制地传递数据,适用于跨越多个层级的组件之间的通信。
- 在todolist中可以在父组件中使用
provide将处理好的list曝光,那么它的后代组件(无论子组件的层次有多深)都可以使用inject访问到。
// 父组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child/>
</template>
<script setup>
import { ref,provide } 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.value)
</script>
<style lang="css" scoped>
</style>
//子组件
<template>
<div class="body">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import {inject} from 'vue';
const list = inject('list');
</script>
<style lang="css" scoped>
</style>
子父通信
这里的场景同样是todolist的添加实现,这里我们将添加input框设置为子组件,来实现子父组件通信。
1. 发布订阅机制
- 子组件可以通过触发一个自定义事件来通知父组件进行相应的操作。
所以为实现场景要求,我们在子组件中创建一个addMsg的事件,通过绑定点击事件来将其携带newMsg.value参数发布,然后在父组件中通过@addMsg来订阅事件,并用handleAddMsg对传入的参数进行添加至数组中,最后渲染。
// 父组件
<template>
<div>
<Child @addMsg="handleAddMsg"></Child>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import Child from "./child.vue";
const list = ref(["html", "css"]);
const handleAddMsg = (value) => {
list.value.push(value);
};
</script>
<style lang="css" scoped></style>
// 子组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newMsg=ref('')
const emit=defineEmits(['addMsg']) //定义事件
const add=()=>{
emit('addMsg',newMsg.value)//发布
}
</script>
<style lang="css" scoped></style>
2. 使用 v-model
v-model实质上是一个语法糖,它负责监听用户的输入事件以更新数据,父组件v-model绑定属性传给子组件,子组件发布updata:xxx事件通知父组件数据更新了。
简单来说,在该场景中我们需要改变的是list数组中的数据,所以就将list用v-model绑定传给子组件,子组件接收到数组后定义update:list事件(使用v-model时,自定义事件必须是update开头),同样通过点击事件来发布,并在点击事件中修改数据后同自定义事件一并发布。发布后,父组件的v-model检查到list数组的数据更新就会使list重新渲染。
tip:这里有很多小伙伴就疑惑了,为什么子组件需要通知父组件?不通知可以吗?
这是因为Vue的设计原则之一是单向数据流,这意味着数据应该从父组件流向子组件,并且任何状态的变化都应该向上反馈给父组件。不反馈通知则将违反这一原则,同时当数据量大的时候很难追踪到出问题的数据。当然,在这个案例中我尝试了直接修改不通知父组件,可以实现效果,但是我发现是因为改变的数据是引用型的,我们改变的是引用地址下的数值,如果我们需要更新的数据不是引用型的就实现不了修改数据,就会造成子父组件中的数据不一样。
// 父组件
<template>
<child v-model:list="list" />
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import child from './child.vue';
import { ref } from 'vue';
const list = ref(['html', 'css'])
</script>
<style lang="css" scoped></style>
// 子组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
list: Array,
default: () => []
})
const newMsg = ref('')
const emit = defineEmits(["update:list"])
const add = () => {
// props.list.push(newMsg.value)
const arr=props.list
arr.push(newMsg.value)
emit("update:list",arr)
}
</script>
<style lang="css" scoped></style>
3. 使用 ref
- 父组件通过ref获取到子组件的dom结构,从而获取到子组件
defineExpose()暴露出来的数据。
这里我在子组件中将添加完的list通过defineExpose()暴露出来,首先我在父组件中打印了一下ref="childRef"发现就是子组件中的list。
// 父组件
<template>
<child ref="childRef"/>
<div class="body">
<ul>
<li v-for="item in childRef?.list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import Child from './child.vue'
import { ref, onMounted } from 'vue';
const childRef= ref(null)
onMounted(() => {
console.log(childRef.value)
})
</script>
<style lang="css" scoped></style>
// 子组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</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})//子组件心甘情愿暴露一个list,响应式的
</script>
<style lang="scss" scoped></style>
总结
总结来说,Vue 提供了多种机制来满足不同场景下的组件通信需求。选择合适的通信方式取决于具体的应用结构和业务逻辑。理解这些机制不仅能帮助我们更好地组织代码,还能提高应用程序的可维护性和扩展性。