深度解析Vue3之组件间通信

157 阅读6分钟

组件通信

1. 【props】

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

  • 父传子:属性值是非函数
  • 子传父:属性值是函数

父组件:

 <template>
   <div class="father">
     <h3>父组件,</h3>
         <h4>我的车:{{ car }}</h4>
         <h4>儿子给的玩具:{{ toy }}</h4>
         <Child :car="car" :getToy="getToy"/>
   </div>
 </template>
 ​
 <script setup lang="ts" name="Father">
     import Child from './Child.vue'
     import { ref } from "vue";
     // 数据
     const car = ref('奔驰')
     const toy = ref()
     // 方法
     function getToy(value:string){
         toy.value = value
     }
 </script>

子组件

 <template>
   <div class="child">
     <h3>子组件</h3>
         <h4>我的玩具:{{ toy }}</h4>
         <h4>父给我的车:{{ car }}</h4>
         <button @click="getToy(toy)">玩具给父亲</button>
   </div>
 </template>
 ​
 <script setup lang="ts" name="Child">
     import { ref } from "vue";
     const toy = ref('奥特曼')
     
     defineProps(['car','getToy'])
 </script>

温馨提示

在面对多级嵌套的组件时进行数据传递,绝对不使用props传给孙组件什么的

2. 【自定义事件】

  1. 概述:自定义事件常用于:子 => 父。
  2. 注意区分好:原生事件、自定义事件。
  • 原生事件:

    • 事件名是特定的(clickmosueenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:

    • 事件名是任意名称
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!
  1. 示例:

     <!--在父组件中,给子组件绑定自定义事件:-->
     <Child @send-toy="toy = $event"/>
     ​
     <!--注意区分原生事件与自定义事件中的$event-->
     <button @click="toy = $event">测试</button>
    
     //子组件中,触发事件:
     this.$emit('send-toy', 具体数据)
    

3.全局事件总线管理工具:mitt

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信,这也像是一个全局事件总线的管理工具mitt,其内存很小(200B/zip),却有很多实用的API

首先,安装mitt工具

 npm i mitt

接着,新建文件:src\utils\emitter.js,在这个文件中可初始配置emitter全局事件管理工具,这样任何组件都可以引入调用这个emitter,从而通过.on绑定事件、emit触发事件、.off解绑事件等操作,来实现任意组件间的通信

 // 引入mitt 
 import mitt from "mitt";
 ​
 // 创建emitter
 const emitter = mitt()
 ​
 /*
   // 绑定事件
   emitter.on('abc',(value)=>{
     console.log('abc事件被触发',value)
   })
   emitter.on('xyz',(value)=>{
     console.log('xyz事件被触发',value)
   })
 ​
   setInterval(() => {
     // 触发事件
     emitter.emit('abc',666)
     emitter.emit('xyz',777)
   }, 1000);
 ​
   setTimeout(() => {
     // 清理事件
     emitter.all.clear()
   }, 3000); 
 */
 ​
 // 创建并暴露mitt
 export default emitter

接收数据的组件中:绑定事件、同时在销毁前解绑事件:

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

在卸载前解绑事件,相当于释放缓存的功能

【第三步】:提供数据的组件,在合适的时候触发事件

 import emitter from "@/utils/emitter";
 ​
 function sendToy(){
   // 触发事件
   emitter.emit('send-toy',toy.value)
 }

注意这个重要的内置关系,总线依赖着这个内置关系

4.组件间双向绑定数据:v-model

  1. 概述:实现 父↔子 之间相互通信。

  2. 前序知识 —— v-model的本质

     <!-- 使用v-model指令 -->
     <input type="text" v-model="userName">
     ​
     <!-- v-model的本质是下面这行代码 -->
     <input 
       type="text" 
       :value="userName" 
       @input="userName =(<HTMLInputElement>$event.target).value"
     >
    
  3. 组件标签上的v-model的本质::moldeValue(父传子) + update:modelValue(子传父)事件。

     <!-- 组件标签上使用v-model指令 -->
     <AtguiguInput v-model="userName"/>
     ​
     <!-- 组件标签上v-model的本质 -->
     <AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>
    

    AtguiguInput组件中:

     <template>
       <div class="box">
         <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
             <!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
         <input 
            type="text" 
            :value="modelValue" 
            @input="emit('update:model-value',$event.target.value)"
         >
       </div>
     </template>
     ​
     <script setup lang="ts" name="AtguiguInput">
       // 接收props
       defineProps(['modelValue'])
       // 声明事件
       const emit = defineEmits(['update:model-value'])
     </script>
    
  4. 也可以更换value,例如改成abc

     <!-- 也可以更换value,例如改成abc-->
     <AtguiguInput v-model:abc="userName"/>
     ​
     <!-- 上面代码的本质如下 -->
     <AtguiguInput :abc="userName" @update:abc="userName = $event"/>
    

    AtguiguInput组件中:

     <template>
       <div class="box">
         <input 
            type="text" 
            :value="abc" 
            @input="emit('update:abc',$event.target.value)"
         >
       </div>
     </template>
     ​
     <script setup lang="ts" name="AtguiguInput">
       // 接收props
       defineProps(['abc'])
       // 声明事件
       const emit = defineEmits(['update:abc'])
     </script>
    
  5. 如果value可以更换,那么就可以在组件标签上多次使用v-model

     <AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
    

5.组件实例化对象属性:$attrs

  1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

    注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

父组件:

 <template>
   <div class="father">
     <h3>父组件</h3>
         <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
   </div>
 </template>
 ​
 <script setup lang="ts" name="Father">
     import Child from './Child.vue'
     import { ref } from "vue";
     let a = ref(1)
     let b = ref(2)
     let c = ref(3)
     let d = ref(4)
 ​
     function updateA(value){
         a.value = value
     }
 </script>

子组件:

 <template>
     <div class="child">
         <h3>子组件</h3>
         <GrandChild v-bind="$attrs"/>
     </div>
 </template>
 ​
 <script setup lang="ts" name="Child">
     import GrandChild from './GrandChild.vue'
 </script>

孙组件:

 <template>
     <div class="grand-child">
         <h3>孙组件</h3>
         <h4>a:{{ a }}</h4>
         <h4>b:{{ b }}</h4>
         <h4>c:{{ c }}</h4>
         <h4>d:{{ d }}</h4>
         <h4>x:{{ x }}</h4>
         <h4>y:{{ y }}</h4>
         <button @click="updateA(666)">点我更新A</button>
     </div>
 </template>
 ​
 <script setup lang="ts" name="GrandChild">
     defineProps(['a','b','c','d','x','y','updateA'])
 </script>

6. 【refs、parent】

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

    属性说明
    $refs值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent值为对象,当前组件的父组件实例对象。

7. 依赖注入provideinject

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据
    • 在后代组件中通过inject配置来声明接收数据
  3. 具体编码:

    【第一步】父组件中,使用provide提供数据

     <template>
       <div class="father">
         <h3>父组件</h3>
         <h4>资产:{{ money }}</h4>
         <h4>汽车:{{ car }}</h4>
         <button @click="money += 1">资产+1</button>
         <button @click="car.price += 1">汽车价格+1</button>
         <Child/>
       </div>
     </template>
     ​
     <script setup lang="ts" name="Father">
       import Child from './Child.vue'
       import { ref,reactive,provide } from "vue";
       // 数据
       let money = ref(100)
       let car = reactive({
         brand:'奔驰',
         price:100
       })
       // 用于更新money的方法
       function updateMoney(value:number){
         money.value += value
       }
       // 提供数据
       provide('moneyContext',{money,updateMoney})
       provide('car',car)
     </script>
    

    注意:子组件中不用编写任何东西,是不受到任何打扰的

    【第二步】孙组件中使用inject配置项接受数据。

    <template>
      <div class="grand-child">
        <h3>我是孙组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="updateMoney(6)">点我</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
      import { inject } from 'vue';
      // 注入数据
     let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
      let car = inject('car')
    </script>
    

8. 【pinia】

这部分内容在我前几篇文章中已有介绍,读者可以自行查看

9.插槽slot

在 Vue 3 中,插槽是一种强大的功能,允许开发者创建灵活的组件结构,以便在组件中插入动态内容。插槽的主要目的是实现组件之间的内容传递,从而提高组件的重用性和灵活性。

插槽(Slots)允许你在父组件中向子组件传递内容。通过插槽,子组件可以在特定位置渲染父组件传入的内容。这种方式类似于模板插值,但提供了更大的灵活性。

插槽理解: 形象的来说,插槽就像是子组件中挖的坑,父组件中使用子组件时,使用插槽技术,可以向这个坑中填入想要渲染的内容

具名插槽:同样,这就像是给子组件中挖好的许多坑赋予各自的名字,在父组件中调用时,可以通过这个名字,来向指定坑中填入渲染的内容

作用域插槽:这是数据在子组件,而操作数据在父组件的一种模式,通过作用域插槽技术,可以在挖坑的时候,便向坑中填入一些数据,但这些数据并没有被使用,在父组件中调用时,可以通过名字找到对应的坑,再通过作用域插槽找到指定的数据,最后在指定的坑中操作指定的数据,进行相应的渲染操作

9.1.默认插槽

默认插槽是最简单的插槽类型。你可以在子组件中定义一个 <slot> 标签,并在父组件中提供内容:

 父组件中:
         <Category title="今日热门游戏">
           <ul>
             <li v-for="g in games" :key="g.id">{{ g.name }}</li>
           </ul>
         </Category>
 子组件中:
         <template>
           <div class="item">
             <h3>{{ title }}</h3>
             <!-- 默认插槽 -->
             <slot></slot>
           </div>
         </template>

9.2.具名插槽

具名插槽允许你定义多个插槽,并为每个插槽指定一个名称。这样,父组件可以选择性地将内容插入到特定的插槽中。

 父组件中:
         <Category title="今日热门游戏">
           <template v-slot:s1>
             <ul>
               <li v-for="g in games" :key="g.id">{{ g.name }}</li>
             </ul>
           </template>
           <template #s2>
             <a href="">更多</a>
           </template>
         </Category>
 子组件中:
         <template>
           <div class="item">
             <h3>{{ title }}</h3>
             <slot name="s1"></slot>
             <slot name="s2"></slot>
           </div>
         </template>

温馨提示

1.v-slot:可以使用#来代替,例如上述例子中的v-slot:s1可以直接替代为#s1

2.事实上,默认插槽的名字为默认的#default

9.3.作用域插槽

理解:数据在子组件(组件的自身),但根据数据生成的结构需要父组件(组件的使用者)来决定,即根据子组件的数据来如何渲染到页面上由父组件决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

 父组件中:
       <Game v-slot="params">
       <!-- <Game v-slot:default="params"> -->
       <!-- <Game #default="params"> -->
         <ul>
           <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
         </ul>
       </Game>
 ​
 子组件中:
       <template>
         <div class="category">
           <h2>今日游戏榜单</h2>
           <slot :games="games" a="哈哈"></slot>
         </div>
       </template>
 ​
       <script setup lang="ts" name="Category">
         import {reactive} from 'vue'
         let games = reactive([
           {id:'asgdytsa01',name:'英雄联盟'},
           {id:'asgdytsa02',name:'王者荣耀'},
           {id:'asgdytsa03',name:'红色警戒'},
           {id:'asgdytsa04',name:'斗罗大陆'}
         ])
       </script>