组件通信:父组件与子组件之间如何传递参数?

865 阅读9分钟

上一次我们一起学习了Vue-Router, 今天我们继续来进阶Vue。我们来聊聊父组件与子组件之间如何传递参数。

我们先来聊聊父子通信,也就是父组件如何向它的子组件传递参数。

1. 父子通信

我们还是使用vite为我们创建一个项目。然后在components文件夹下创建一个demo1文件夹,作为我们今天的第一个案例,然后在demo1文件夹下创建一个 child.vue和 parent.vue 文件。

然后在 parent.vue 文件中引入 child.vue 文件作为一个组件使用,那parent.vue就是父组件,child.vue就是子组件。

我们来实现这样一个例子来认识父子组件之间是如何进行参数传递的。

image.png

我在input框输入的内容,当我点击添加时就会出现在下面的列表里。

这个业务逻辑我们之前写过一次,很简单对不对。我们先要获取用户在input框输入的内容,用v-model就可以获取。然后提前准备一个数组,这个数组得是响应式的,我们可以使用ref声明它。还要给‘添加’按钮绑定一个点击事件,当用户点击了‘添加’按钮时,事件就会触发,将用户输入的内容push到数组里,数组中的值我们可以v-for遍历放到ul中的li里面。

这样就能完成这个业务逻辑。这些操作我们完全可以在父组件中完成。但现在我不想这么干,我想把input框和button按钮放到父组件中完成,把列表项放到子组件中完成,然后将子组件引入到父组件中使用。此时这两个组件之间的逻辑应该怎么联系起来呢?

也就是这样,parent.vue的代码:

<template>
    <div class="hd">
        <input type="text" v-model="inputValue">
        <button @click="add">添加</button>
    </div>
    <child></child>

</template>

<script setup>
import child from './child.vue'
import { ref } from 'vue'

const inputValue = ref('')

const add = () => {

}
</script>

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

child.vue的代码:

<template>
    <div class="bd">
        <ul>
            <li v-for="item in list" :key="item">{{ item }}</li>
        </ul>
    </div>
</template>

<script setup>
import { ref } from 'vue'

const list = ref(['html', 'css', 'js'])
</script>

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

我将inputValue作为用户在input框中输入的内容,并且把list数组放到子组件中去了,然后将child组件引入到父组件中。此时这个业务逻辑还能生效吗?当然是不能的,因为inputValue是在父组件里的变量,而list数组是在子组件中的数组,我怎么才能把父组件中的inputValue添加到子组件中的list数组中去呢?换句话来说,如果我们能把父组件中的inputValue添加到子组件中的list数组中去,这个业务逻辑不就正常了吗?

所以问题就来到了然后才能将父组件中的变量传递给子组件。

我们可以这么干,在父组件中:

<template>
    <div class="hd">
        <input type="text" v-model="inputValue">
        <button @click="add">添加</button>
    </div>
    <child :msg="data"></child>

</template>

<script setup>
import child from './child.vue'
import { ref } from 'vue'

const inputValue = ref('')
const data = ref('')

const add = () => {
    data.value = inputValue.value
    inputValue.value = ''
}
</script>

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

我们再创建一个响应式变量data,在用户点击了‘添加’按钮之后,触发了add事件,在add事件中就将inputValue.value赋值给data.value。这一步的操作是为了获取用户完整的输入,当用户点击了‘添加’按钮之后,说明此时inputValue存放的就是用户想添加到列表中的完整内容,我们就把它再存放到data中一次性传给子组件。

此时问题就来到了如何将data传给子组件。我们这样操作,在child标签上用v-bind绑定一个属性msg,这个msg我们可以自己随便定义,然后值为data。这样就实现了父向子传参。然后在子组件中我们需要接收这个参数,用defineProps接收。

在子组件中:

<template>
    <div class="bd">
        <ul>
            <li v-for="item in list" :key="item">{{ item }}</li>
        </ul>
    </div>
</template>

<script setup>
import { ref } from 'vue'

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

const list = ref(['html', 'css', 'js'])
list.value.push(props.msg)
</script>

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

defineProps就是专门用来接收父组件中传过来的值,父组件传过来了一个msg,子组件就接收一个msg,类型为String,默认值为空。然后我们将props.msg添加到list数组中去。这样就行了吗?我们来试一下。

PixPin_2024-12-18_13-42-32.gif

我们发现并没有效果。这是为什么呢?难道父组件中的值没有传过来吗?其实是传过来了的。这是因为我们在父组件用v-bind绑定了一个动态属性msg,值为data。当data的值发生了改变,msg中的逻辑就会执行,子组件中的msg就能接收到这个新的值。但是因为只要data中的值发生了变化,msg中的逻辑就会重新执行一遍,msg的初始值为空,而push操作只会执行一次,它就还是会将这个空字符push到list数组中去。虽然你的值发生了更改,但我push操作只会执行一次,所以还是会将空字符push到list数组中。

所以在子组件中,我们需要去监听props.msg的变化,只要你发生了改变,push的操作就要执行一次。

那在vue中如何去监听一个值的变化呢?是不是可以使用computed或者watch去监听啊,我们也讲过的。这里我们使用watch去监听props.msg的变化。

在子组件中:

<template>
    <div class="bd">
        <ul>
            <li v-for="item in list" :key="item">{{ item }}</li>
        </ul>
    </div>
</template>

<script setup>
import { ref, watch } from 'vue'

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

const list = ref(['html', 'css', 'js'])
watch(
    () => props.msg,
    (newVal, oldVal) => {
        list.value.push(newVal)
    }
)
</script>

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

只要props.msg发生了变化,就push到list中去。这样就行了。

PixPin_2024-12-18_14-02-02.gif

这样就实现了父组件向子组件传参。

这种方法是可以实现的,但不优雅,我们可以换一种更优雅的传参方式。第一种方法我们是将用户输入的内容传了过去,这一次我们将list数组放到父组件中,我们直接将list数组传过去。

这样写的好处是什么呢?当用户输入了内容之后,我们在父组件中将内容push到list数组中,然后将这个已经发生了变更的数组传递给子组件,是不是就不用考虑list数组在子组件中的变化了。我已经将改好的数组传递给你了,你直接v-for展示就好了,不需要考虑其它的。这样写代码量就会减少很多。

所以在父组件中:

<template>
    <div class="hd">
        <input type="text" v-model="inputValue">
        <button @click="add">添加</button>
    </div>
    <child :list="list"></child>

</template>

<script setup>
import child from './child.vue'
import { ref } from 'vue'

const inputValue = ref('')
const list = ref(['html', 'css', 'js'])

const add = () => {
    list.value.push(inputValue.value)
    inputValue.value = ''
}

</script>

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

我们直接在add函数中完成push操作,然后给child标签v-bind绑定一个动态属性list,值就为list数组。当list数组中的值发生了变化,list属性的逻辑就会执行一遍,子组件就会接收一遍。

所以在子组件中用defineProps接收了之后,直接展示就行了,因为是已经更改好了的。

子组件:

<template>
    <div class="bd">
        <ul>
            <li v-for="item in list" :key="item">{{ item }}</li>
        </ul>
    </div>
</template>

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

</script>

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

这样同样完成了这个业务逻辑。

第二种方法代码的数量相比第一种更简洁了,也更好理解。但其实两种方法的逻辑都是一样的,都是父组件传参给子组件展示。

所以对于父组件向子组件传参,我们有这样一个结论:

父组件通过v-bind绑定属性将自己的数据传递给子组件,子组件通过defineProps接收父组件绑定的属性

2. 子父通信

讲完了父组件如何向子组件传参,我们当然得来看看子组件如何向父组件传参。

还是这个业务逻辑,只不过这次我们将input框和button按钮放在子组件中,列表项放到父组件中,还是将子组件引入到父组件中去使用。

父组件:

<template>
  <child/>
  <div class="bd">
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
  </div>

</template>

<script setup>
import child from './child.vue'
import { ref } from 'vue'

const list = ref(['html', 'css', 'js'])
</script>

<style lang="scss" scoped>

</style>

子组件:

<template>
  <div class="hd">
    <input type="text" v-model="inputValue">
    <button @click="add">添加</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const inputValue = ref('')

const add = () => {


  inputValue.value = '' // 清空输入框
}
</script>

<style lang="css" scoped>

</style>

此时,问题就变成了子组件如何向父组件传参了,因为我们是在子组件中接收用户的输入的。

那怎么才能将inputValue传递给父组件呢?

我们还能用我们父子通信时用的方法吗?在父子通信时,我们是在父组件中的child标签上动态绑定了一个属性,而在这里,子组件中有parent标签吗?没有,因为我们不是将父组件引入到子组件中使用,而是子组件引入到父组件中使用,父组件能使用子组件作为标签使用,而子组件不能使用父组件作为标签使用。

那有人说我更改一下身份不就行了,那更改身份了不还是父组件向子组件传参吗。所以我们不能使用父子通信时使用的方法,得换一种方法。

我们可以在子组件中定义一个事件,在父组件中接收这个事件。当用户点击‘添加’按钮后,触发add事件,我们在add事件中发布这个事件,而事件的发布是能够携带参数的,父组件就能接收这个参数。

那我们怎么去定义一个事件呢?我们需要从vue中引入一个函数,defineEmits,我们调用它来定义一个事件,它会接收一个数组,数组里放上事件名。然后在add函数中发布该事件。

子组件:

<template>
  <div class="hd">
    <input type="text" v-model="inputValue">
    <button @click="add">添加</button>
  </div>
</template>

<script setup>
import { ref, defineEmits } from 'vue'

const inputValue = ref('')

const emit = defineEmits(['adds']) // 定义一个事件
const add = () => {
  // inputValue传给父组件
  emit('adds', inputValue.value) // 发布该事件,并传递参数
  inputValue.value = '' // 清空输入框
}
</script>

<style lang="css" scoped>

</style>

我们发布事件的时候还带了一个事件参数inputValue.value,这样当有人绑定了这个事件,它就一定能访问这个事件参数。那我们就要让父组件绑定这个事件,这样它就能访问inputValue.value了,就能将它push到list数组中。

那我们直接在父组件中的child标签上绑定这个事件adds值为adds,也叫订阅这个事件。

父组件:

<template>
  <child @adds="adds" />

  <div class="bd">
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
  </div>

</template>

<script setup>
import child from './child.vue'
import { ref } from 'vue'

const list = ref(['html', 'css', 'js'])

const adds = (e)=>{
  list.value.push(e)
}
</script>

<style lang="css" scoped>

</style>

我们就把这个adds拿下来写成一个函数,它就会带来一个事件参数,这个事件参数是形参,我们可以随便取。它就是子组件中的inputValue.value,于是我们就将它push到list数组中取。

这样就实现了子向父传参。

PixPin_2024-12-18_22-51-44.gif

所以对于子父通信,我们有一个结论:

子组件通过defineEmits定义事件,父组件通过@事件名="事件处理函数"监听子组件的事件(订阅行为),子组件通过emit发布事件,并传递参数(发布行为)

3. 总结

本篇文章我们一起学习了一下父组件与子组件之间如何传递参数,其实组件通信之间还有兄弟组件或者不相关组件之间如何传递参数的问题,那就要用到仓库了。我们下次再聊,如果本篇文章对你有帮助的话不妨点个赞吧。