• Vue3 学习-组件通讯(二)



    前言

    本文主要记录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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 子组件
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过props获取的数据是代理对象
    props的数据是只读的,不可修改

    在这里插入图片描述

    二、自定义事件(emit)

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

    前置

    • 在vue2中在子组件添加原生事件时默认为自定义事件,需要通过.native修饰符变为原生DOM事件
    • 在Vue3中组件添加原生事件时为原生事件,当子组件绑定时变为自定义事件
    • 原生事件会带一个event回调函数用来记录当前事件的一些属性。
    const handler2 = (event) => {
      //event为事件对象
      console.log(event)
    }
    
     <h1>事件</h1>
        <pre @click="handler2">
          点击原生事件event
        </pre>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    事件event


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

    <hr>
      <Event1 @click="handler3"/>
      <hr>
      <Event2 @event2="handler4"/>
    
    • 1
    • 2
    • 3
    • 4

    通过defineEmits接收自定义事件

    let emit = defineEmits(['click'])
    
    let emit = defineEmits(['event2']);
    
    • 1
    • 2
    • 3

    利用回调函数向父组件传值,其中第一个参数为自定义事件名,之后可带多个参数

    const handler =()=>{
      emit('click','向父组件传值1')
    }
    
    const handler = () => {
      emit('event2','传给父组件2')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    回调参数

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

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

    自定义事件


    三、全局事件总线(EventBus)

    不提倡使用

    Vue2中使用$bus可进行全局事件的通讯

    使用步骤:

    • 创建vue实例
    import Vue from 'vue'
    export const bus = new Vue()
    
    • 1
    • 2
    • 在B组件想发送数据,定义this.$bus.$emit
    import {bus} from "../../utils/bus.js"
    // 这里自己修改路径,且bus.js文件中导出的是export所以使用import {bus}
    ...
    // 发送数据
    bus.$emit(参数1'定义一个方法名', 参数2'要发送的数据')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 在A组件想接收数据,定义this.$bus.$onthis.$bus.$off
    import {bus} from "../../../utils/bus.js"
    ...
    // 接收数据
    bus.$on(参数1'$emit的方法名', 参数2:'function(value){
        // value是接收到的数据
    }')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 清除
    eventBus.$off('方法名', {})
    
    • 1

    在Vue3中没有全局事件总线,可通过插件mitt来实现全局总线事件,mitt不依赖 Vue 实例,所以可以跨框架使用,mitt里面封装了4个方法:

    • all:所有的全局事件
    • emit:发布事件
    • off:移除事件监听
    • on :接收事件

    mitt方法

    使用

    • 安装并创建文件挂载
    pnpm add mitt
    
    //引入mitt插件:mitt一个方法,触发执行会返回bus对象
    import mitt from 'mitt';
    //挂在在$bus变量上
    const $bus = mitt();
    //导出
    export default $bus;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 组件发布事件
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 组件使用接收
    import $bus from '../../bus'
    import {onMounted} from "vue";
    //组件挂载完成时,为荡秋千组件绑定一个事件,接收兄弟组件传递的数据
    onMounted(()=>{
      //第一个参数:事件类型
      //第二个参数:事件回调
      $bus.on('car',(car)=>{
        console.log(car)
      })
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    可以看到该插件使用map来存事件的,key就是事件名称,value则是一个回调函数,所以,all也继承了map的方法,可以通过其方法对事件进行操作。

    //取消监听
    $bus.off("car")
    //如果发送了多个,可监听全部
    $bus.on("*",(type,val) = >{
       console.log(type,val)   //type就是类型 之前注册的getDetail
    })
    //取消所有监听
    $bus.all.clear();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    四、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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    子组件代码:

    <template>
      <input type="text" v-model="msg">
    </template>
    <script setup>
    import { defineModel } from 'vue'
    let msg = defineModel('msg')
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以在一个组件上绑定多个v-model
    相当于给组件绑定了 update:pageNoupdate:pageSize 的自定义事件

    <Child2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"/>
    
    • 1

    子组件代码

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    五、userAttrs

    vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件

    该方法可以获取在组件 标签上的原生和自定义事件,

    <HintButton type="primary" size="small" :icon="Edit"></HintButton>
    
    • 1

    userAttrs

    例子:

    父组件

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    子组件

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    被props获取的属性和属性值不会被useAttrs获取到


    六、ref和$parent

    通过ref和$parent可以获取DOM实例对象,就可以进行通讯了

    ref和$parent获取到的DOM实例是获取不到内部定义的数据的,所以要通过 defineExpose 暴露出去

    • ref(父传子)

    在子标签上添加ref

    <Son ref="son"/>
    
    • 1

    再声明同名变量,此时son就是子组件实例

    let son = ref()
    
    • 1

    在子组件中暴露需要传递的数据或者方法

    defineExpose({
      money,
      fly
    })
    
    • 1
    • 2
    • 3
    • 4

    在父组件中使用

    const handler = () => {
      money.value+=10;
      console.log(son)
      //操作子组件数据
      son.value.money-=10
      son.value.fly()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • $parent(子传父)

    在子组件中定义事件并传入 $parent 作为参数,此时$parent就是父组件实例

    <button @click="handler($parent)">我是子组件2按钮</button>
    
    const handler = ($parent) => {
      money.value+=1000
      console.log($parent)
      //操作父组件数据
      $parent.money-=1000
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    父组件对外暴露数据或者方法

    //对外暴露
    defineExpose({
      money
    })
    
    • 1
    • 2
    • 3
    • 4

    完整代码

    父组件

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    $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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    七、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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以在其他组件中接收数据

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果接收的是个响应式数据可以在接收组件改变其数据


    八、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
    
    • 1
    • 2
    • 3

    选择式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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    定义组合式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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    总结

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

  • 相关阅读:
    JS 这次真的可以禁止常量修改了!
    获取阿里云Docker镜像加速器
    SpringBoot-组件配置/自动配置原理/Lombok
    Deep Residual Learning for Image Recognition
    Vue源码学习(十三):实现watch(一):方法,对象
    限流与下载接口请求数控制
    使用telegram机器人发送通知
    C++ Qt开发:LineEdit单行输入组件
    Qt + FFmpeg 搭建 Windows 开发环境
    WPF 使用动画绘制一个点赞大拇指
  • 原文地址:https://blog.csdn.net/smznbhh/article/details/131671966