一篇文章弄懂父子组件通信!

675 阅读7分钟

引言

在Vue中,父子组件的通信是组件间交互的基础。而父子组件间的通信又有好几种情况。本文将通过几个示例详细讲解如何在父子组件之间进行通信。

父子通信

父组件传值,子组件接收

在 Vue 中,父组件可以通过属性(props)向子组件传递数据,子组件使用 defineProps 接收这些数据。示例如下。 parent.vue

<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>

child.vue

<template>
    <div class="body">
      <ul>
        <li v-for="item in props.list">{{item}}</li>
      </ul>
    </div>
</template>

<script setup>
  const props =  defineProps({
        list:{
            type:Array,
            default: () => []

        }
    })
</script>

<style lang="scss" scoped>

</style>

ViteApp2--MicrosoftEdge2024-07-2314-07-31-ezgif.com-video-to-gif-converter.gif

我们可以看到当在输入框输入后,点击按钮触发事件,传给子组件的数组也随着改变了。 这是最常用的一种父子通信方式,本质就是父组件将每次修改后的数组传给子组件,子组件再渲染出来。其中父组件通过 v-model 绑定输入框的值 parentMessage,并将其传递给子组件的 message 属性。 子组件使用 defineProps 接收 message 并进行渲染。

父组件修改子组件属性

父组件通过修改子组件的 props 来触发子组件的更新。示例如下:

parent.vue

<template>
    <div class="header">
      <input type="text" v-model="newMsg">
      <button @click="add">确定</button>
    </div>
  
    <Child :msg="val" />
  </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>

child.vue

<template>
    <div class="body">
      <ul>
        <li v-for="item in list">{{item}}</li>
      </ul>
      <!-- <p>{{ msg }}</p> -->
    </div>
</template>

<script setup>
import {ref , computed, watch,onBeforeUpdate,onUpdated} from 'vue'
const list = ref(['html','css'])

const props =  defineProps({
    msg: {
      type: String,
      default: ''
    }

  })



    // watch(
    //     () =>props.msg,
    //     (newVal,oldVal) => {
    //       list.value.push(newVal)
    //     }
    // )

    onBeforeUpdate(() => {
        // console.log(props.msg); 在html用到的变量变更了就会执行
        list.value.push(props.msg)
    })

    // onUpdated(() => {
    //     console.log(props.msg);
    //     list.value.push(props.msg)
    //     // 会一直执行,因为子组件中的list更改,onUpdated会一直执行


    // })

 

</script>

<style lang="scss" scoped>

</style>

在这个示例中,我们在父组件的输入框绑定了 newMsg ,每当点击按钮触发了事件,就会同时去修改与子组件绑定的 msg 的值,当子组件的属性发生改变后触发响应,将修改后的值添加进要渲染的数组,达成效果。

而我们去实现发生改变有响应的事件,可以使用 watch去监听 或钩子函数。

  • watch 是 Vue 的一个响应式系统,它用于监听数据的变化,并在变化时执行特定的操作。 这里我们是用watch 正确追踪到 props.msg 的变化,再用回调函数,将新的 props.msg 值添加到 list 中,实现效果。

  • onBeforeUpdate 是在组件即将被更新之前调用的钩子函数,onUpdated 是在组件更新之后调用的钩子函数。 onBeforeUpdate 用于在组件即将更新时将 props.msg 的值推入 list 中。这是因为 onBeforeUpdate 会在任何响应式数据变化导致组件更新之前执行。

    而为什么不用 onUpdated 是因为onUpdated 钩子会在组件更新后触发,而在这个钩子中,我们又将 props.msg 推入 list,这会导致组件再次更新,触发 onUpdated 钩子,再次将 props.msg 推入 list,如此循环不止,陷入死循环。

    这便是用 onUpdated 陷入死循环

ViteApp2--MicrosoftEdge2024-07-2314-33-20-ezgif.com-video-to-gif-converter.gif

如下便是使用 watchonBeforeUpdate 实现的效果

ViteApp2--MicrosoftEdge2024-07-2314-30-47-ezgif.com-video-to-gif-converter.gif

provideinject 跨层级传递

provideinject 是 Vue 中用于跨层级组件传递数据的机制。但是只能允许祖先组件向后代组件传递数据,而且无需通过一层层的 props 传递。示例如下:

parent.vue

<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>

child.vue

<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="scss" scoped>

</style>

Parent.vue 中,我们使用 provide 提供了 list 数组,这个 list 可以被所有后代组件注入和使用。 在 Child.vue 中,我们使用 inject 方法接收父组件 Parent 提供的 list ,并渲染。

ViteApp4--MicrosoftEdge2024-07-2315-20-18-ezgif.com-video-to-gif-converter.gif

子父通信

事件发布订阅机制

Vue中可以通过父子组件之间的事件订阅和发布,实现数据和事件的传递。示例如下:

parent.vue

<template>
    <Child @addMsg="handle" />
  
    <div class="body">
      <ul>
        <li v-for="item in list">{{item}}</li>
      </ul>
    </div>
  </template>
  
  <script setup>
  import { ref } from 'vue';
    import Child from './child.vue';
  const newMsg = ref('')
  const list = ref(['html', 'css'])
  const handle = (e) => {
    list.value.push(e)
  }
  
  </script> 
  
  <style lang="css" scoped>
  
  </style>

child.vue

<template>
    <div class="header">
      <input type="text" v-model="newMsg">
      <button @click="add">确定</button>
    </div>
</template>

<script setup>
import { ref,defineEmits } from 'vue';
    const newMsg = ref('')
    const emits = defineEmits(['addMsg'])  // 定义事件

    const add = () => {
        emits('addMsg',newMsg.value)  // 发布 ,顺序是先订阅后发布

    }
</script>

<style lang="scss" scoped>

</style>

defineEmits 用于声明子组件可以触发的事件。

在这个示例中,本质上是子组件传值给父组件中的 list ,再在父组件中渲染出来。

  • 子组件中 const emits = defineEmits(['addMsg']) 定义了一个名为 addMsg 的事件。在 add 方法中,通过 emits('addMsg', newMsg.value) 触发 addMsg 事件,并传递 newMsg.value 作为参数。 当用户点击按钮时,add 方法被调用,发布事件 addMsg,并将输入框中的数据传递给父组件。

  • 在父组件中,通过 @addMsg="handle" 监听子组件触发的 addMsg 事件。 当子组件触发 addMsg 事件时,父组件的 handle 方法被调用。handle 方法接收子组件传递的数据 msg。 在 handle 方法中,将接收到的数据添加到 list 中再进行。

使用 v-model 绑定

这个相当于是上文中的事件订阅发布机制的一种优化。v-model 在 Vue 中通常用于双向绑定数据。在这个例子中,它将父组件的 list 数据与子组件的 list prop 绑定在一起,简化了数据的传递和更新逻辑。

parent.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 { ref } from 'vue';
  import Child from './child.vue'

  
 
  const list = ref(['html', 'css'])
  
 
  </script>
  
  <style lang="css" scoped>
  
  </style>

child.vue

<template>
    <div class="header">
      <input type="text" v-model="newMsg">
      <button @click="add">确定</button>
    </div>
</template>

<script setup>
import { ref , defineProps,defineEmits} from 'vue'
const newMsg = ref('')
const emits = defineEmits(['update:list'])


const add = () => {
    // props.list.push(newMsg.value)
    // 不推荐
    const arr = props.list
    arr.push(newMsg.value)
    emits('update:list',arr)
}
const props = defineProps({
  list:[]
})

</script>

<style lang="scss" scoped>

</style>

在父组件中,使用 v-model:list="list" 将数据 list 绑定到子组件 Childlist prop

在子组件中,使用 defineEmits(['update:list']) 来定义组件可以触发的事件,并通过 emits('update:list', newMsg.value) 触发事件,并将 newMsg.value 作为参数传递给父组件,父组件的 list 会被更新,从而达成效果。

可以看到与事件订阅发布相比, parent.vue 简化了许多,方法都不需要了,只需要绑定就好了。

注意事项 不推荐直接修改 props 的原因是Vue 强调单向数据流,即数据应该从父组件流向子组件。props 是用于父子组件通信的一种机制,设计上是为了让父组件将数据传递给子组件,而子组件则负责接收这些数据并进行展示或处理。直接修改 props 违反了这种单向数据流的原则,因为它使得子组件可以修改由父组件传递来的数据,导致数据流动变得不明确和难以追踪。

父组件通过 ref 获取子组件实例

父组件可以通过 ref 获取子组件的实例,从而访问子组件暴露出来的数据和方法。此方式比较简单,直接在子组件中修改 list ,然后再暴露传递给父组件,父组件获取后再渲染。示例代码如下:

parent.vue

<template>
    
    <Child ref="childRef" />
  
    <div class="body">
      <ul>
        <li v-for="item in childRef?.list">{{item}}</li>
      </ul>
    </div>
  </template>
  
  <script setup>
    import { onMounted } from 'vue';
import Child from './child.vue'
    import { ref } from 'vue'

    const childRef = ref(null)

    onMounted(() => {
        console.log(childRef.value.list)
    })

  </script>
  
  <style lang="css" scoped>
  
  </style>

child.vue

<template>
     <div class="header">
      <input type="text" v-model="newMsg">
      <button @click="add">确定</button>
    </div>
</template>

<script setup>
import { ref , defineExpose} 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>
  • 子组件使用 defineExpose 暴露 message 数据。

  • 父组件通过 ref 获取子组件实例,并在 showMessage 方法中访问子组件的 message 数据

总结

通过以上示例,我们可以看到在 Vue 中父子组件通信的多种方式,每种方式都有特定的使用场景:

  • propsdefineProps:用于父组件向子组件传递数据。
  • 修改子组件属性:父组件通过修改 props 来触发子组件更新。
  • inject:用于祖先组件向后代组件传递数据。
  • 事件机制:子组件通过事件向父组件传递数据。
  • v-modeldefineEmits:用于父组件与子组件之间的双向数据绑定。
  • refdefineExpose:用于父组件访问子组件的实例和数据。

理解和掌握这些通信方式,可以帮助我们在开发中更好地组织和管理 Vue 组件,如果觉得这篇文章对你有用,可以点个赞哦。