文章目录
- 前言
- 一、props通信
- 二、自定义事件(emit)
- 三、全局事件总线(EventBus)
- 四、v-model
- 五、userAttrs
- 六、ref和$parent
- 七、Provide与Inject
- 八、pinia
- 九、slot插槽
- 总结
前言
本文主要记录Vue3的九种组件通信方式
一、props通信
子组件需要用defineProps方法接收父组件传递的数据,该方法不用引入,为Vue3自带,直接使用
- 父组件
<script setup lang="ts">
import Son from "./Son.vue";
import {ref} from "vue";
let money = ref<Number>(1000)
</script>
<template>
<Son info="我是父组件" :money="money"/>
</template>
- 子组件
<script setup lang="ts">
//需要用defineProps方法接收父组件传递的数据
//该方法不用引入,为Vue3自带,直接使用
//通过props获取的数据是代理对象
//props的数据是只读的,不可修改
let props = defineProps(['info','money'])
console.log(props)
</script>
<template>
<div>
<p>{{info}}</p>
<p>{{money}}</p>
</div>
</template>
通过props获取的数据是代理对象
props的数据是只读的,不可修改
二、自定义事件(emit)
子组件需要用defineEmits方法接收父组件传递的自定义事件,引入后可向父组件传值,该方法不用引入,为Vue3自带,直接使用
前置
- 在vue2中在子组件添加原生事件时默认为自定义事件,需要通过.native修饰符变为原生DOM事件
- 在Vue3中组件添加原生事件时为原生事件,当子组件绑定时变为自定义事件
- 原生事件会带一个event回调函数用来记录当前事件的一些属性。
const handler2 = (event) => {
//event为事件对象
console.log(event)
}
<h1>事件</h1>
<pre @click="handler2">
点击原生事件event
</pre>
父组件在子组件标签上绑定自定义事件,当绑定的为原生事件时,如果子组件没有使用,则默认为原生事件,二子组件使用则为自定义事件。
<hr>
<Event1 @click="handler3"/>
<hr>
<Event2 @event2="handler4"/>
通过defineEmits接收自定义事件
let emit = defineEmits(['click'])
let emit = defineEmits(['event2']);
利用回调函数向父组件传值,其中第一个参数为自定义事件名,之后可带多个参数
const handler =()=>{
emit('click','向父组件传值1')
}
const handler = () => {
emit('event2','传给父组件2')
}
父组件使用,用函数接收参数
const handler3 = (param) => {
console.log('接收到子组件1的值:'+param)
}
const handler4 = (param) => {
console.log('接收到子组件2的值:'+param)
}
三、全局事件总线(EventBus)
不提倡使用
Vue2中使用$bus可进行全局事件的通讯
使用步骤:
- 创建vue实例
import Vue from 'vue'
export const bus = new Vue()
- 在B组件想发送数据,定义
this.$bus.$emit
import {bus} from "../../utils/bus.js"
// 这里自己修改路径,且bus.js文件中导出的是export所以使用import {bus}
...
// 发送数据
bus.$emit(参数1:'定义一个方法名', 参数2:'要发送的数据')
- 在A组件想接收数据,定义
this.$bus.$on
和this.$bus.$off
import {bus} from "../../../utils/bus.js"
...
// 接收数据
bus.$on(参数1:'$emit的方法名', 参数2:'function(value){
// value是接收到的数据
}')
- 清除
eventBus.$off('方法名', {})
在Vue3中没有全局事件总线,可通过插件mitt来实现全局总线事件,mitt不依赖 Vue 实例,所以可以跨框架使用,mitt里面封装了4个方法:
- all:所有的全局事件
- emit:发布事件
- off:移除事件监听
- on :接收事件
使用
- 安装并创建文件挂载
pnpm add mitt
//引入mitt插件:mitt一个方法,触发执行会返回bus对象
import mitt from 'mitt';
//挂在在$bus变量上
const $bus = mitt();
//导出
export default $bus;
- 组件发布事件
<script setup lang="ts">
import $bus from "../../bus";
const handler = () => {
$bus.emit('car',{car:"法拉利"})
}
</script>
<template>
<div class="child2">
<p>我是子组件2</p>
<button @click="handler">点击给兄弟组件1传值</button>
</div>
<div></div>
</template>
- 组件使用接收
import $bus from '../../bus'
import {onMounted} from "vue";
//组件挂载完成时,为荡秋千组件绑定一个事件,接收兄弟组件传递的数据
onMounted(()=>{
//第一个参数:事件类型
//第二个参数:事件回调
$bus.on('car',(car)=>{
console.log(car)
})
})
可以看到该插件使用map来存事件的,key就是事件名称,value则是一个回调函数,所以,all也继承了map的方法,可以通过其方法对事件进行操作。
//取消监听
$bus.off("car")
//如果发送了多个,可监听全部
$bus.on("*",(type,val) = >{
console.log(type,val) //type就是类型 之前注册的getDetail
})
//取消所有监听
$bus.all.clear();
四、v-model
在Vue3.3中,简化了子传父的代码,利用defineModel完成defineEmit的代码。
v-model为组件注册了自定义事件update:modelValue,并在props的setter调用了该事件modelValue,从而实现v-model的语法糖。
父组件代码:
<template>
<div>
{{msg}}
</div>
<Child1 v-model="msg"/>
</template>
<script setup>
import { ref } from 'vue'
import Child1 from "../04_v-model/Child1.vue";
// 第一次父组件传给子组件的值
const msg =ref(4)
</script>
子组件代码:
<template>
<input type="text" v-model="msg">
</template>
<script setup>
import { defineModel } from 'vue'
let msg = defineModel('msg')
</script>
可以在一个组件上绑定多个v-model
相当于给组件绑定了 update:pageNo 和 update:pageSize 的自定义事件
<Child2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"/>
子组件代码
<script setup lang="ts">
import { defineModel } from 'vue'
let No = defineModel()
// 接收参数
let props = defineProps(['pageNo','pageSize'])
let page = defineEmits(['update:pageNo','update:pageSize'])
// 子组件回调传值
const headler = () => {
page("update:pageNo",props.pageNo +6)
page("update:pageSize",props.pageSize +6)
}
</script>
<template>
<div class="child2">
<p>同时绑定多个v-model</p>
<div>{{props.pageNo}}</div>
<div>{{props.pageSize}}</div>
<Button @click="headler"></Button>
</div>
<div></div>
</template>
五、userAttrs
vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件
该方法可以获取在组件 标签上的原生和自定义事件,
<HintButton type="primary" size="small" :icon="Edit"></HintButton>
例子:
父组件
<script setup lang="ts">
// vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件
import {Edit} from '@element-plus/icons-vue'
import HintButton from "./HintButton.vue";
</script>
<template>
<div>
<h1>userAttrs</h1>
<el-button type="primary" size="small" :icon="Edit" ></el-button>
<!-- 自定义组件-->
<HintButton type="primary" size="small" :icon="Edit"></HintButton>
</div>
</template>
<style scoped>
</style>
子组件
<script setup lang="ts">
//引入useAttrs方法:获取标签上属性与事件
import {useAttrs} from "vue"; //返回对象
let $attrs = useAttrs();
// 如果使用props接收属性和属性值,则useAttrs是接收不到的
//因为props的优先级高于useAttrs
console.log($attrs)
</script>
<template>
<div>
<el-button :="$attrs">子组件</el-button>
</div>
</template>
<style scoped>
</style>
被props获取的属性和属性值不会被useAttrs获取到
六、ref和$parent
通过ref和$parent可以获取DOM实例对象,就可以进行通讯了
ref和$parent获取到的DOM实例是获取不到内部定义的数据的,所以要通过 defineExpose 暴露出去
- ref(父传子)
在子标签上添加ref
<Son ref="son"/>
再声明同名变量,此时son就是子组件实例
let son = ref()
在子组件中暴露需要传递的数据或者方法
defineExpose({
money,
fly
})
在父组件中使用
const handler = () => {
money.value+=10;
console.log(son)
//操作子组件数据
son.value.money-=10
son.value.fly()
}
- $parent(子传父)
在子组件中定义事件并传入 $parent
作为参数,此时$parent就是父组件实例
<button @click="handler($parent)">我是子组件2按钮</button>
const handler = ($parent) => {
money.value+=1000
console.log($parent)
//操作父组件数据
$parent.money-=1000
}
父组件对外暴露数据或者方法
//对外暴露
defineExpose({
money
})
完整代码
父组件
<script setup lang="ts">
//ref:获取真实DOM结点,可以获取到子组件实例VC
//$parent:可以在子组件内部获取到父组件的实例
import {ref} from 'vue'
import Son from "./Son.vue";
import Daughter from "./Daughter.vue";
let money = ref(100000000)
//获取子组件实例
let son = ref()
const handler = () => {
money.value+=10;
console.log(son)
//操作子组件数据
son.value.money-=10
son.value.fly()
}
//对外暴露
defineExpose({
money
})
</script>
<template>
<div class="box">
<h1>ref与$parent</h1>
<h2>我是父组件:{{money}}</h2>
<button @click="handler">增加父组件数值减少子组件数值</button>
<hr>
<!-- ref形式-->
<Son ref="son"/>
<hr>
<!-- $parent形式-->
<Daughter />
</div>
</template>
<style scoped>
.box{
width: 100vw;
height: 500px;
background: skyblue;
}
</style>
ref子组件
<script setup lang="ts">
import {ref} from "vue";
let money = ref(666)
const fly = ()=>{
console.log('我是子内部组件方法')
}
//组件内部数据对外是关闭的,其他组件不能访问
//如果想让外部访问需要通过defineExpose方法对外暴露
defineExpose({
money,
fly
})
</script>
<template>
<div class="son">
<h2>我是子组件:{{money}}</h2>
</div>
</template>
<style scoped>
.son{
width: 300px;
height: 200px;
background: cyan;
}
</style>
$parent子组件
<script setup lang="ts">
import {ref} from "vue";
let money = ref(2000)
//事件回调传入参数 $parent 来获取父组件实例
//父组件必须将数据对外暴露
const handler = ($parent) => {
money.value+=1000
console.log($parent)
//操作父组件数据
$parent.money-=1000
}
</script>
<template>
<div class="dau">
<h1>我是子组件2</h1>
<button @click="handler($parent)">我是子组件2按钮</button>
</div>
</template>
<style scoped>
.dau{
width: 300px;
height: 300px;
background: hotpink;
}
</style>
七、Provide与Inject
依赖注入
vue3 提供了provide(提供)与inject(注入),可以实现隔辈组件通讯
利用了原型链,在父组件provides上创建新对象:Object.create().parentProvides
子组件使用时在原型链上查找
- provide是个方法提供两个参数,提供数据
提供数据的key
组件提供的数据 - inject通过注入祖先提供的数据,利用key值来获取数据
注:后辈组件是可以修改祖先组件传过来的的值,并影响祖先组件的数据
个人建议:对于子改变父组件数据尽量不要用,保证数据的单向流动,防止父组件数据更改后影响其他组件
以下为提供的两个将数据变为非响应式数据的方法
- toRaw:把一个响应式对象转化为普通对象
- markRaw:把某个数据,标记为普通对象,当我们把它放到响应式对象中,也依然是非响应式的
在任意组件中注入数据
let car = ref<String>("大众")
type Car = {
car:string
}
type CarData = {
[index: number]:Car
}
let cars:CarData = reactive([{car:'大众'},{car:'宝马'}])
const TOKEN = Symbol() as InjectionKey<CarData>
provide(TOKEN,cars)
可以在其他组件中接收数据
<script setup lang="ts">
import {inject} from "vue";
let cars = inject<string>('TOKEN')
console.log(cars)
const hander=()=>{
cars[0].car = '奔驰'
}
</script>
<template>
<div class="child2">
<h1>孙子组件</h1>
<p v-for="item of cars">{{item.car}}</p>
<button @click="hander">改变</button>
</div>
</template>
如果接收的是个响应式数据可以在接收组件改变其数据
八、pinia
在vue3之前状态管理使用VueX来实现,现在官方更加推荐pinia在vue3中实现状态管理
vuex:集中式管理状态容器,可以实现任意组件之间的通讯 核心:
1、state 存储数据
2、mutations 唯一修改数据
3、actions 处理异步、处理业务
4、getters 计算属性
5、modules 模块式开发
pinia:集中式管理状态容器,可以实现任意组件之间的通讯 核心:
1、state 存储数据
2、actions 修改数据、处理异步、处理业务
3、getters 计算属性
利用createPinia方法创建大仓库
并对外暴露该仓库
在全局引入(文件位置:./src/store)
import {createPinia} from "pinia";
let store = createPinia()
export default store
选择式API仓库
defineStore方法定义小仓库,带两个参数
1、仓库名称
2、仓库配置对象
defineStore返回一个函数,让组件可以获取到仓库数据
存储数据:state
需对外暴露方法
import {defineStore} from "pinia";
let userInfoStore = defineStore("info",{
state:()=>{
return {
count: 99,
arr: [1,2,3,4,5,6,7,8,9,10]
}
},
actions: {
//内部没有context上下文对象
//没有commit、没有mutations去修改数据
updateNum(a:number,b:number){
this.count+=(a+b)
}
},
getters: {
total() {
let result:number = this.arr.reduce((prev,next)=>{
return prev+next
},0)
return result
}
}
})
export default userInfoStore
定义组合式API仓库
务必返回一个对象:属性与方法可以提供给组件使用
import {defineStore} from "pinia";
import {computed, reactive} from "vue";
let userTodoStore = defineStore("todo",()=>{
let todos = reactive([{id:1,title:'吃饭'},{id:2,title:'睡觉'}])
let arr = reactive([1,2,3,4,5,6])
const total = computed(()=>{
return arr.reduce((prev,next)=>{
return prev+next
},0)
})
return {
todos,
total,
updateTodo(){
todos.push({id:3,title: '组合式API'})
}
}
})
export default userTodoStore
写法:
1、选择式
修改数据:
-
使用返回的函数直接修改其属性
infoStore.count++ -
使用返回函数上的
$patch
方法
infoStore.$patch({count:222}) -
使用自定义方法,在actions中定义方法,可传参
infoStore.updateNum()
仓库:this.count++
注:在方法内部要用this,this指向仓库对象
2、组合式
修改数据:
-
使用返回的函数直接修改其属性
todoStore.todos[0].title = ‘喝水’ -
使用computed计算属性,将计算值返回就能获取
const total = computed(()=>{
return arr.reduce((prev,next)=>{
return prev+next
},0)
}) -
使用自定义方法,在return中定义方法,可传参
updateTodo(){
todos.push({id:3,title: ‘组合式API’})
}
注:在方法内部要用this,this指向仓库对象
九、slot插槽
插槽分为三种插槽:
- 默认插槽
在子组件直接使用slot标签<slot></slot>
在父组件中子组件标签里添加传递的结构 - 具名插槽
在子组件直接使用slot标签<slot name="a"></slot>
在父组件中子组件标签里添加template传递的结构,并用v-slot:(简写: #)指定是那个插槽
<template v-slot:a>
<pre>早死晚死都得死</pre>
</template>
- 作用域插槽
可传递数据的插槽,子组件可以将数据回传给父组件,父组件可以定义该数据以某种方式或者外观在子组件内部展示
子组件可以在slot标签上传递数据<slot :row="item" :index="index"></slot>
父组件在template用v-slot用接收数据并定义子组件内部显示方式
<template v-slot="{row,index}">
<span :style="{color:row.done?'green':'red'}">{{row.title}}--{{index}}</span>
</template>
总结
本文主要记录了vue3中九种组件通讯的方法与示例。