方式一:props
「父」向「子」组件发送数据
父组件:
定义需要传递给子组件的数据,并使用 v-bind 指令将其绑定到子组件的 props 上。
<template>
<child-component :message="parentMessage" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
import { ref } from 'vue'
let parentMessage = ref('111')
</script>
子组件:
使用defineProps声明接收props。
<template>
<div>{{ message }}</div>
</template>
<script setup>
defineProps(['message'])
</script>
「子」向「父」组件发送数据
父组件:
定义一个函数,参数为子组件中的数据,并使用 v-bind 指令将其绑定到子组件的 props 上。
<template>
<child-component :sendData="getSonData"/>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
// 定义一个函数,用于接收子组件中的数据
function getSonData(data){
console.log("子组件中的数据:",data)
}
</script>
子组件:
defineProps接收props,调用父组件中声明的函数,并把数据当做参数传入。
<template>
<button @click="sendData(data)">把数据发送到父组件</button>
</template>
<script setup>
import { ref } from 'vue'
let data = ref('111') // 子组件中的数据
defineProps(['sendData']) // 接收props
</script>
方式二:自定义事件
适用于「子」向「父」组件发送数据
父组件:
定义一个函数,参数为子组件中传递的数据,自定义事件名,并将函数传给子组件。
<template>
<child-component @send-data="getSonData"/>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
// 定义一个函数,用于接收子组件中的数据
function getSonData(data){
console.log("子组件中的数据:",data)
}
</script>
子组件:
defineEmits声明事件,调用父组件中声明的事件回调函数,并把数据当做参数传入。
emit函数第一个参数为事件回调函数名,后续参数为该回调函数的参数。
<template>
<button @click="emit('send-data',data)">把数据发送到父组件</button>
</template>
<script setup>
import { ref } from 'vue'
let data = ref('111') // 子组件中的数据
const emit = defineEmits(['send-data']) // 接收props
</script>
方式三:mitt
适用于所有组件间通信,将事件统一管理
类似vue2中的全局事件总线$bus以及pubsub消息订阅发布模式。由于Vue3中没有全局事件总线,所以需要使用mitt插件替代。
接收数据方:订阅消息(绑定好事件)
发送数据方:发布消息(在合适时机触发事件)
下载mitt
npm i mitt
根据官方提示,创建一个名为emitter的文件,其中创建并暴露emitter,在入口文件main.js中引用emitter。
emitter.js
// 引入mitt
import mitt from 'mitt'
// 调用mitt得到emitter,可以绑定、触发事件
const emitter = mitt()
// 暴露emitter
export default emitter
emitter实例中有四个方法,分别为emit「触发事件」、on「绑定事件」、off「解绑事件」、all「所有绑定事件」。
接收数据方:定义事件及回调函数
<template>
<div>数据:{{ data }}</div>
<MyComp />
</template>
<script setup>
import { ref } from 'vue'
import MyComp from "./MyComp.vue"
import emitter from './emitter.js' // 引入
let data = ref('')
// 绑定send-data事件
emitter.on('send-data',(value)=>{
data.value = value
})
</script>
发送数据方:触发事件并传入数据
<template>
<button @click="emitter.emit('send-data',data)">发送数据</button>
</template>
<script setup>
import { ref } from 'vue'
import emitter from './emitter.js' // 引入
let data = ref('111')
</script>
注意
:接收数据方,需要在组件卸载时,及时解绑事件,避免内存泄漏。
import { onUnmounted } from 'vue'
onUnmounted(()=>{
emitter.off('send-data') // 解绑指定事件
})
方式四:v-model
首先需要知道的是:v-model是一个语法糖,其实现流程分为v-bind(数据 => 视图)以及v-on(视图 => 数据),从而实现数据与视图的双向绑定。
下面以input举例说明
<template>
<input type="text" v-model="user_name" />
<!-- v-model原始写法 -->
<input
type="text"
:value="user_name"
@input="user_name = (<HTMLInputElement>$event.target).value"
/>
</template>
<script setup lang="ts" name="Home123">
import { ref } from "vue"
let user_name = ref("")
</script>
<style scoped></style>
当Input封装为组件时,v-model的原始写法
<template>
<MyInput v-model="user_name" />
<MyInput :modelValue="user_name" @update:modelValue="user_name = $event" />
</template>
父组件
<template>
<MyInput v-model="user_name" />
</template>
<script setup lang="ts" name="Home123">
import { ref } from "vue"
import MyInput from "./MyInput.vue"
let user_name = ref("")
</script>
<style scoped></style>
自定义Input组件
<template>
<input
type="text"
:value="modelValue"
@input="emits('update:modelValue', (<HTMLInputElement>$event.target).value)"
/>
</template>
<script setup lang="ts" name="MyInput123">
defineProps(["modelValue"])
let emits = defineEmits(["update:modelValue"])
</script>
<style scoped>
input {
width: 200px;
height: 40px;
background-color: antiquewhite;
border: 1px solid #efefef;
border-radius: 5px;
}
</style>
v-model默认以 :modelValue 以及 @update:modelValue 两步实现。
如果绑定多个v-model就需要自定义属性名,可以在v-model后添加 :属性名
<template>
<MyInput v-model:user="user_name" />
</template>
子组件中将原先modelValue变为自定义属性名
<template>
<input
type="text"
:value="user"
@input="emits('update:user', (<HTMLInputElement>$event.target).value)"
/>
</template>
<script setup lang="ts" name="MyInput123">
defineProps(["user"])
let emits = defineEmits(["update:user"])
</script>
子组件中Input的 $event到底是什么,以及什么时候可以.target。
对于原生事件来说, $event就是事件对象。
对于自定义事件来说, $event就是触发事件时,所传递的数据。
方式五:$attrs
适用于当前组件的父组件 和 当前组件的子组件通信,便于理解可以视为「祖孙」间通信
父组件
<template>
<div class="father">
父组件
<Son :a="a" :b="b" :data="{ c, d }" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son from "./Son.vue"
let a = ref("a")
let b = ref("b")
let c = ref("c")
let d = ref("d")
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
子组件
<template>
<div class="son">
子组件 --- 父值a:{{ a }}
<Sun />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Sun from "./Sun.vue"
defineProps(["a"])
</script>
<style scoped>
.son {
border: 1px solid skyblue;
border-radius: 10px;
padding: 20px;
}
</style>
子组件中未通过defineProps接收的值,会出现在attrs。
可以在子组件中,将 $attrs 再传递给子组件的子组件,也就是「孙组件」。
<template>
<div class="son">
子组件 --- 父值a:{{ a }}
<Sun v-bind="$attrs" />
</div>
</template>
切换到「孙组件」,可以看到在子组件中接收的a并没有一起传递。
孙组件根据attrs中属性名,接收值
<template>
<div class="sun">孙组件</div>
<div>b --- {{ b }}</div>
<div>c --- {{ data.c }}</div>
<div>d --- {{ data.d }}</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
defineProps(["b", "data"])
</script>
<style scoped>
.sun {
border: 1px solid #f5d8dc;
border-radius: 10px;
padding: 20px;
}
</style>
至此,「祖」向「孙」方向数据传递完毕,「孙」向「祖」同props传递一个函数即可。
父组件
<template>
<div class="father">
父组件
<Son :a="a" :b="b" :data="{ c, d }" :changeD="changeD" /> // 函数传递
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son from "./Son.vue"
let a = ref("a")
let b = ref("b")
let c = ref("c")
let d = ref("d")
// 定义函数
function changeD(params: string) {
d.value = params
}
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
子组件中由于未接收除a外的props,只是把attrs传递给孙组件,所以无需修改,此时attrs中多了父组件中刚刚定义好的函数。
孙组件 定义触发函数,传递数据
<template>
<div class="sun">孙组件</div>
<div>b --- {{ b }}</div>
<div>c --- {{ data.c }}</div>
<div>d --- {{ data.d }}</div>
<button @click="changeD('e')">改变</button> // 触发函数并将参数传给祖组件
</template>
<script setup lang="ts">
import { ref } from "vue"
defineProps(["b", "data", "changeD"]) // 接收祖组件中的函数
</script>
<style scoped>
.sun {
border: 1px solid #f5d8dc;
border-radius: 10px;
padding: 20px;
}
</style>
方式六:$refs $parent
适用于父组件与多个子组件间的通信
子组件中通过defineExpose将数据暴露
<template>
<div class="son">子组件1 ---- {{ son1_data }}</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
let son1_data = ref("111")
// 把数据交给外部
defineExpose({ son1_data })
</script>
<style scoped>
.son {
border: 1px solid skyblue;
border-radius: 10px;
padding: 20px;
}
</style>
父组件中通过ref,直接修改子组件中数据
<template>
<div class="father">
父组件
<Son1 ref="s1" />
<Son2 />
<button @click="changeSonData">修改子数据</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son1 from "./Son1.vue"
import Son2 from "./Son2.vue"
let s1 = ref()
function changeSonData() {
s1.value.son1_data = "父组件修改后的数据"
}
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
如果需要同时修改多个子组件,可以使用 $refs
父组件
<template>
<div class="father">
父组件
<button @click="changeSonData">修改子数据</button>
<button @click="getAllChild($refs)">getall</button>
<Son1 ref="s1" />
<Son2 ref="s2" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son1 from "./Son1.vue"
import Son2 from "./Son2.vue"
let s1 = ref()
let s2 = ref()
function changeSonData() {
s1.value.son1_data = "父组件修改后的数据"
}
function getAllChild(refs: object) {
console.log(refs)
}
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
触发getall按钮,发现打印如下,即所有绑定ref的元素
可以循环该参数,批量修改子组件中同名数据
function getAllChild(refs: { [key: string]: any }) {
for (const key in refs) {
refs[key].money += 10
}
}
子组件通过$parent可以修改父组件中的值
<button @click="getMoney($parent)">获取money</button>
function getMoney(parent: any) {
parent.money -= 10
}
父组件中暴露数据
defineExpose({ money })
方式七:provide inject
适用于任意组件间通信,以下以爷爷组件与孙组件通信为例
父组件:provide提供数据,第一个参数为数据标识名,第二个参数为数据值。
<template>
<div class="father">
父组件
<div>余额:{{ money }}</div>
<Son />
</div>
</template>
<script setup lang="ts">
import { provide, ref } from "vue"
import Son from "./Son.vue"
let money = ref(1000)
function reduceMoney(value: number) {
money.value -= value
}
provide("balanceContext", { money, spendMoney: reduceMoney }) // 提供数据
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
如果下载了扩展,自动补全了.value请手动删除,这么做会丢失响应式。
子组件:不同于$attrs,无需进行任何操作
<template>
<div class="son">
子组件
<Sun />
</div>
</template>
<script setup lang="ts">
import Sun from "./Sun.vue"
</script>
<style scoped>
.son {
border: 1px solid skyblue;
border-radius: 10px;
padding: 20px;
}
</style>
孙组件:inject注入数据,第一个参数为数据标识名,要与provide中命名一致,第二个参数为默认值。
<template>
<div class="sun">孙组件</div>
<div>爷爷的余额 --- {{ money }}</div>
<button @click="spendMoney(1)">花爷爷的钱</button>
</template>
<script setup lang="ts">
import { inject, ref } from "vue"
const { money, spendMoney } = inject("balanceContext", {
money: 0,
spendMoney: (p: number) => {},
}) // 注入数据
</script>
<style scoped>
.sun {
border: 1px solid #f5d8dc;
border-radius: 10px;
padding: 20px;
}
</style>
方式八:Slot
在子组件中的展示内容需要依赖父组件中的数据的场景下,就可以使用插槽。
插槽分为三种,分别是默认插槽、具名插槽以及作用于插槽
默认插槽
父组件:在子组件标签内部写结构
<template>
<div class="father">
<h2>父组件</h2>
<Son :title="title">
<ul>
<li v-for="item in list" :key="item.id">{{ item.verses }}</li>
</ul>
</Son>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son from "./Son.vue"
let title = ref("蝶恋花·阅尽天涯离别苦")
let list = ref([
{ id: 1, verses: "待把相思灯下诉" },
{ id: 2, verses: "一缕新欢" },
{ id: 3, verses: "旧恨千千缕" },
{ id: 4, verses: "最是人间留不住" },
{ id: 5, verses: "朱颜辞镜花辞树" },
])
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
子组件:使用slot标签占位,接收父组件中标签内部的结构,如为空,展示slot标签内部内容。
<template>
<div class="son">
<h3>子组件</h3>
<div>{{ title }}</div>
<slot>默认值</slot>
</div>
</template>
<script setup lang="ts">
defineProps(["title"])
</script>
<style scoped>
.son {
border: 1px solid skyblue;
border-radius: 10px;
padding: 20px;
}
</style>
具名插槽
默认插槽只支持一块模版,如果子组件中需要多个slot,就需要通过标识区分这些插槽,这就是具名插槽。
父组件:将多个插槽内容分别包裹template标签,添加v-slot属性,:后是插槽名。
注意
:老版本v-slot=""的写法不再推荐了,建议使用 :插槽名。
<template>
<div class="father">
<h2>父组件</h2>
<Son>
<template v-slot:t1>
<h4>{{ title }}</h4>
</template>
<template v-slot:t2>
<ul>
<li v-for="item in list" :key="item.id">{{ item.verses }}</li>
</ul>
</template>
</Son>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son from "./Son.vue"
let title = ref("蝶恋花·阅尽天涯离别苦")
let list = ref([
{ id: 1, verses: "待把相思灯下诉" },
{ id: 2, verses: "一缕新欢" },
{ id: 3, verses: "旧恨千千缕" },
{ id: 4, verses: "最是人间留不住" },
{ id: 5, verses: "朱颜辞镜花辞树" },
])
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
通过编译器警告可以发现,v-slot只能用于组件或者template标签上。
v-slot: 语法糖写法为 #
子组件:slot标签中添加name属性,值需与父组件中v-slot:后的插槽名保持一致。
<template>
<div class="son">
<h3>子组件</h3>
<slot name="t1">默认标题</slot>
<!-- 子组件中的元素 -->
<div>清·王国维</div>
<slot name="t2">默认内容</slot>
</div>
</template>
<script setup lang="ts">
defineProps(["title"])
</script>
<style scoped>
.son {
border: 1px solid skyblue;
border-radius: 10px;
padding: 20px;
}
</style>
作用域插槽
如果渲染结构依赖的数据在子组件中,但是结构需要在父组件中声明。子组件就可以使用作用域插槽,将变量通过插槽传递给父组件。
需求:现有Son子组件,其中有数据list。父组件中需要展示三次子组件,分别以ul、ol、以及div标签遍历子组件中的list,并渲染。
子组件:将数据以slot属性的形式传递。
<template>
<div class="son">
<h3>子组件</h3>
<slot :list="list" user="田本初"></slot>
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue"
let list = reactive([
{ id: 1, verses: "待把相思灯下诉" },
{ id: 2, verses: "一缕新欢" },
{ id: 3, verses: "旧恨千千缕" },
{ id: 4, verses: "最是人间留不住" },
{ id: 5, verses: "朱颜辞镜花辞树" },
])
</script>
<style scoped>
.son {
border: 1px solid skyblue;
border-radius: 10px;
padding: 20px;
}
</style>
父组件:使用v-slot接收参数,其中包含了slot标签上所有props。
<template>
<div class="father">
<h2>父组件</h2>
<Son>
<template v-slot="params">
<h4>{{ title }}</h4>
<ul>
<li v-for="item in params.list" :key="item.id">{{ item.verses }}</li>
</ul>
</template>
</Son>
<Son>
<!-- 可以解构 -->
<template v-slot="{ list }">
<h4>{{ title }}</h4>
<ol>
<li v-for="item in list" :key="item.id">{{ item.verses }}</li>
</ol>
</template>
</Son>
<Son>
<template v-slot="params">
<h4>{{ title }}</h4>
<div v-for="item in params.list" :key="item.id">{{ item.verses }}</div>
</template>
</Son>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Son from "./Son.vue"
let title = ref("蝶恋花·阅尽天涯离别苦")
</script>
<style scoped>
.father {
border: 1px solid #333;
border-radius: 10px;
padding: 20px;
}
</style>
如果使用的是具名插槽
子组件:slot标签中添加name属性
<slot name="son" :list="list" user="田本初"></slot>
父组件:template标签的v-slot后添加 :name,也可以使用语法糖。
<template v-slot:son="params"></template>
<template #son="params"></template>
方式九:Pinia
目前最适用于大量数据以及组件间通信的方式
配置与使用具体可参考另一篇文章:搭建Pinia环境及其基本使用,其中有详细说明。
总结
父传子
- Props
- v-model
- $refs
- 默认插槽、具名插槽
子传父
- Props
- 自定义事件
- v-model (Props方式的语法糖)
- $parent (优势:通过 $refs 父组件可同时操作多个子组件)
- 作用域插槽
祖孙通信
- $attrs(父传子,子不接收props继续传给孙组件)
- provide inject (无需通过子组件,父组件可以直接传递给任意后代组件)
任意组件间通信
- mitt
- pinia
推荐