Vue3 学习-组件通讯(二)

文章目录

  • 前言
  • 一、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的数据是只读的,不可修改

Vue3 学习-组件通讯(二)_第1张图片

二、自定义事件(emit)

子组件需要用defineEmits方法接收父组件传递的自定义事件,引入后可向父组件传值,该方法不用引入,为Vue3自带,直接使用

前置

  • 在vue2中在子组件添加原生事件时默认为自定义事件,需要通过.native修饰符变为原生DOM事件
  • 在Vue3中组件添加原生事件时为原生事件,当子组件绑定时变为自定义事件
  • 原生事件会带一个event回调函数用来记录当前事件的一些属性。
const handler2 = (event) => {
  //event为事件对象
  console.log(event)
}

 <h1>事件</h1>
    <pre @click="handler2">
      点击原生事件event
    </pre>

Vue3 学习-组件通讯(二)_第2张图片


父组件在子组件标签上绑定自定义事件,当绑定的为原生事件时,如果子组件没有使用,则默认为原生事件,二子组件使用则为自定义事件。

<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')
}

Vue3 学习-组件通讯(二)_第3张图片

父组件使用,用函数接收参数

const handler3 = (param) => {
  console.log('接收到子组件1的值:'+param)
}
const handler4 = (param) => {
  console.log('接收到子组件2的值:'+param)
}

Vue3 学习-组件通讯(二)_第4张图片


三、全局事件总线(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.$onthis.$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 :接收事件

Vue3 学习-组件通讯(二)_第5张图片

使用

  • 安装并创建文件挂载
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)
  })
})

Vue3 学习-组件通讯(二)_第6张图片

可以看到该插件使用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:pageNoupdate: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>

Vue3 学习-组件通讯(二)_第7张图片

例子:

父组件

<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指向仓库对象

    Vue3 学习-组件通讯(二)_第8张图片

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标签
    在父组件中子组件标签里添加template传递的结构,并用v-slot:(简写: #)指定是那个插槽
<template v-slot:a>
    <pre>早死晚死都得死</pre>
  </template>
  • 作用域插槽
    可传递数据的插槽,子组件可以将数据回传给父组件,父组件可以定义该数据以某种方式或者外观在子组件内部展示
    子组件可以在slot标签上传递数据
    父组件在template用v-slot用接收数据并定义子组件内部显示方式
 <template v-slot="{row,index}">
          <span :style="{color:row.done?'green':'red'}">{{row.title}}--{{index}}</span>
        </template>

总结

本文主要记录了vue3中九种组件通讯的方法与示例。

你可能感兴趣的:(vue学习,学习,vue.js)