vue中的组件通信——父子组件

1,652 阅读5分钟

在现代前端开发中,Vue.js 是一种非常流行的选择。Vue 提供了一套简洁、灵活且强大的 API,使得开发者能够高效地构建复杂的应用程序。在 Vue 应用中,组件是基本的构建单元,而组件间的通信则是应用架构设计的关键部分之一。在最近的学习中,已经初步的在了解一个完整的项目的开发,发现存在很多情况都涉及到了组件之间的传参问题,也了解到很多可以现实的方法,在本文中我将做一个系统性的归纳总结。

父子通信

现在来一个场景,实现一个简单的todolist添加效果,要求将list做成子组件实现展示list。

image.png

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

image.png

// 父组件
<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 提供了多种机制来满足不同场景下的组件通信需求。选择合适的通信方式取决于具体的应用结构和业务逻辑。理解这些机制不仅能帮助我们更好地组织代码,还能提高应用程序的可维护性和扩展性。