Vue3-组件通信

1.props传递

父传子

数据是单项绑定的,属于只读。

<!-- 父组件传递apple -->
<Son :apple="apple"/>
// 子组件接收apple
const props = defineProps(['apple'])


子传父

<!-- 父组件传递sendOrange方法到子组件 -->
<Son :sendOrange="getOrange"/>
// 子组件接收方法,调用时传递参数
const props = defineProps(['sendOrange'])
props.sendOrange('橘子')
// 父组件的getOrange方法被调用,参数已经拿到了
function getOrange(val: string){
  console.log(`我吃了儿子的一口${val}`)
}

2.自定义事件

子传父

<!-- 父组件绑定自定义事件sendOrange到子组件 -->
<Son @sendOrange="getOrange"/>
// 子组件使用defineEmits定义sendOrange方法
const emit = defineEmits(['sendOrange'])
onMounted(() => {
  setTimeout(() => {
    emit('sendOrange', '橘子') // 携带参数触发sendOrange的回调
  }, 2000)
})
// 父组件中自定义事件sendOrange的回调getOrange方法被触发
function getOrange(val){
  console.log(`我吃了儿子的一口${val}`)
}

3.mitt传递

全局通信

// 引入mitt
npm i mitt
// src/utils/emmiter.ts
// 创建并暴露emmiter.ts
import mitt from "mitt"

const emitter = mitt() // 这个相当于工具,身上带有各种方法比如 on emit off all 

export default emitter
// src/components/Son1.vue
import emitter from "@/utils/emitter"; // 引入emitter 
onMounted(() => {
  // 在哥哥组件里订阅消息,准备获取弟弟组件的数据
  emitter.on('getSmallBroData',(val: any) => {
    youngerBroData.value = val
  })
})
// 组件卸载时记得取消订阅消息
onBeforeMount(() => {
  emitter.off('getSmallBroData')
})
// src/components/Son2.vue
import emitter from "@/utils/emitter";
onMounted(() => {
  setTimeout(() => {
    // 三秒后发布消息,发送数据到哥哥那边
    emitter.emit('getSmallBroData', '弟弟的数据')
  }, 3000);
})

4.v-model传递(*重点*)

理解

自定义封装Input组件时,使用v-model就属于父子组件互相传递参数,拆解后分为两点:普通props传递与自定义事件传递,将这两点组合起来,再进行简写,就是v-model传递了,常用在输入类组件的自定义封装。

使用方法

假如我现在要自定义封装一个input框,我应该怎么做呢?

  1. 创建一个普通子组件:TestInput.vue。
  2. 在父组件中引入子组件<test-input/>。
  3. 注意,要封装的是input组件,就像Element-UI中的<el-input v-model="xxx">一样,我们也应该写成<test-input v-model="xxx"/>。
    // src/components/Father.vue
    <template>
      <test-input v-model="data"/>
    </template>
    <script setup lang="ts">
    import { ref } from "vue";
    import TestInput from "@/components/TestInput.vue";
    const data = ref('默认值')
    </script>
    <style scoped>
    </style>
  4. 子组件应该如何接收这个v-model呢?这里直接提供答案,在后面的底层逻辑会详细讲解。(这里都是固定写法哦,命名是不能改变的,这是由Vue3开发者规定的)
    // src/components/TestInput.vue
    <template>
    <input 
      type="text" 
      :value="modelValue" 
      @input="emit('update:modelValue', $event.target.value)"
    >
    </template>
    <script setup lang="ts">
    defineProps(['modelValue'])
    const emit = defineEmits(['update:modelValue'])
    </script>
    
    
  5. 创建多个具有多个v-model的组件TestForm.vue
    v-model命名:
    // src/components/Father.vue
    <test-form v-model:name="nameVal" v-model:age="ageVal"/>
    接收v-model:
    // src/components/TestForm.vue
    <input 
      type="text" 
      :value="name" 
      @input="emit('update:name', $event.target.value)"
    />		
    <input  
      type="text" 
      :value="age" 
      @input="emit('update:age',$event.target.value)"
    />			
    
    // src/components/TestForm.vue
    defineProps(['name','age'])
    const emit = defineEmits(['update:name', 'update:age']
    
    

底层原理

没研究过底层逻辑的朋友看到上面的使用方法会一脸懵B,
有疑问的朋友别急,先看这个例子:

<input type="text" v-model="data"/>

把v-model换成它最基本的样子:

<input type="text" :value="data" @input="data=$event.target.value"/>
  1. :value="data"  实现了单向绑定,将数据呈现到了页面上;
  2. @input="data=$event.target.value"  随着input事件被触发,将最新的值赋值给data。

这两种相扶而成,就成为了数据的双向绑定,而v-model也成为了这种写法的简写形式。

那么如果是自定义的input组件该怎么写?跟原生input很相似,有些许的不同:

<test-input :modelValue="data" @update:modelValue="data = $event"/> 
  1. :modelValue="data"  向子组件传递props参数。(命名由Vue3开发者决定,写死了。)
  2. @update:modelValue="data = $event"  给子组件绑定自定义事件,当这个自定义方法被调用时,就将$event赋值给data。(注意!它的事件名是带有分号的,这不是你没有学过的新语法,命名由Vue3开发者决定,写死了。)

    所以朋友们,我们再结合这段代码来看看:

    // src/components/TestInput.vue
    <template>
    <input 
      type="text" 
      :value="modelValue" 
      @input="emit('update:modelValue', $event.target.value)"
    >
    </template>
    <script setup lang="ts">
    defineProps(['modelValue'])
    const emit = defineEmits(['update:modelValue'])
    </script>
    
    
    1. modelValue是什么?
      解答:就只是个props参数而已,用之前记得defineProps声明一下。
    2. update:modelValue又是什么?
      解答:一个带分号的自定义事件名。
    3. input的事件回调为什么要触发update:modelValue这个方法?
      解答:简单来说,父向子传递两个参数(简写形式为v-model),一个是数据(modelValue),一个是自定义方法(update:modelValue),子拿着数据来展示,拿着自定义方法等待着@input事件触发,一旦触发,子会将最新数据通过这个自定义方法传递给父,实现了父子组件之间的数据通信。
    4. 执行顺序为?
      解答:
      ①父向子传入props参数用于初始化子组件的:value;父中给子绑定自定义事件,在其回调中写入驱动data实时改变的代码并等待执行;
      <!-- 这里的回调有几种写法,官方推荐第一种 -->
      <!-- 为什么这里不能$event.target.value? -->
      <!-- 1.因为这个是自定义组件而不是input元素-->
      <!-- 2.$event的值就是自定义方法使用emit调用时传递的参数的值 -->
      <test-input :modelValue="data" @update:modelValue="data = $event;"/> 
      <!-- 其他写法 -->
      <test-input :modelValue="data" @update:modelValue="($event) => {data = $event}"/> 
      <test-input :modelValue="data" @update:modelValue="(val) => {data = val}"/> 
      ②用户敲下键盘,子组件中原生input事件被触发,在原生事件@input的回调中使用emit手动触发自定义方法,并向父传递最新数据($event.target.value)。
      ③父组件在自定义方法被触发后的回调中使用$event接收参数,对data进行赋值,结束该事件流,完成了利用v-model的数据传递。

      5.$attrs传参

      理解

      浅层级组件以props方式向更深层级组件传参,在此过程中,被defineProps定义的参数将会从$attrs中取出并放在对应组件的props中,而未被defineProps定义的参数将会在$attrs中继续往更深层级传递。

      使用场景

      跨级传参:父传孙或者孙传父

      使用方法

      // 父组件:向子组件传递3个参数
      <Child :fatherData=fatherData :getGrandChildData="getGrandChildData" :x="123"/>
      
      // 子组件:定义x参数并使用v-bind加$attrs的方式继续向下传递剩余参数
      <grandChild v-bind="$attrs"/>
      const props = defineProps(['x'])
      
      // 孙组件:能拿到剩余2个参数并使用,拿不到x参数了。
      const props = defineProps(['fatherData', 'getGrandChildData'])

      6.$refs与$parents传参 

      理解

      非常简单,事件传参时使用$refs占位符能获取所有被ref属性标记的子组件实例,使用$parents能获取父组件实例,需要特别注意的是:不管是$refs还是$parents,拿到对应组件实例后不等同拿到了它的数据,要记得在组件实例中使用defineExpose({xxx})将数据暴露出去。

      使用场景

      $refs:父组件中批量修改子组件的状态

      $parents:子组件中修改父组件的状态

      使用方法

      $refs:

      <!-- 父组件:$refs包含父组件中所有被ref标记的组件实例 -->
      <Child1 ref="child1Ref"/>
      <Child2 ref="child2Ref"/>
      <button @click="changeChildData($refs)">点我批量更改子组件的数据</button>
      // 父组件:批量修改子组件状态
      function changeChildData($refs:any){
        Object.keys($refs).forEach(keyName => {
      	$refs[keyName].childData = '我们虽然不是同一个子组件,但我们有同一个爸爸!'
        })
      }
      // 子组件:
      const childData = ref('我是Child可以暴露的数据')
      defineExpose({childData})

       $parents:

      // 父组件
      const fatherData = ref(0)
      defineExpose({fatherData})
      <!-- 子组件:$parent就是父组件实例 -->
      <button @click="changeFatherData($parent)">点我更新父亲的数据</button>
      // 子组件:修改父组件的状态
      function changeFatherData($parent:any){
        $parent.fatherData+=1
      }

      7.provide与inject传参 

      理解

      provide 和 inject 是用于实现依赖注入的 API。它们的主要作用是在组件树中跨层级传递数据或方法,而不需要通过 props 逐层传递。这种方式特别适合在深层嵌套的组件之间共享数据或逻辑。

      使用场景

      1. 跨层级组件通信:

        父组件需要向深层嵌套的子组件(如孙组件、曾孙组件等)传递数据或方法,避免通过 props 逐层传递,简化代码结构。

      2. 共享全局状态:

        在组件树的顶层提供全局状态(如用户信息、主题配置等),供所有子组件使用。

      3. 插件或工具方法注入:

        在根组件中提供一些工具方法或插件,供所有子组件使用。

       使用方法

      1. provide:用于在祖辈组件中提供数据或方法,供后代组件注入使用。

        // 祖辈组件
        import { provide, ref } from 'vue';
        const message = ref('Hello from parent');
        provide('message', message);
      2. inject:用于在后代组件中注入祖辈组件提供的数据或方法。

        // 子组件
        import { inject } from 'vue';
        // 注入数据
        const message = inject('message', 'Default Message');

      注意:如果遇到重名的情况,Vue 会以就近原则为准,即会使用最近的父级组件提供的同名属性进行注入。这意味着如果有多个父级组件提供了同名属性,那么被注入的属性将取决于组件树中的层次关系。Vue 不会报错,但是可能会导致意外的结果,因此在设计组件时应尽量避免出现重名的情况,或者使用更具有唯一性的命名方式。 

      8.slot插槽 

      理解

      1. 什么是插槽?

      插槽是 Vue 组件中的一个占位符,允许父组件向子组件传递内容(HTML 结构、文本、其他组件等)。子组件通过插槽定义内容的位置,父组件通过插槽填充内容。

      2. 插槽的作用

      ①内容分发:将父组件的内容插入到子组件的指定位置。

      ①组件复用:通过插槽,组件可以更灵活地适应不同的使用场景。

      解耦 UI 和逻辑*:子组件负责获取或管理数据,但不同父组件可能需要用不同方式展示这些数据。

      使用场景

      封装灵活度的组件会用到

      使用方法

      这里以封装一个简易的模态框为例子,父组件负责控制HTML解构和样式,子组件只处理逻辑和获取数据。
      父组件:

      1. #header其实就是v-slot:header的缩写,其余插槽也一样。
      2. 这里没写默认插槽,如果是默认插槽想要接收子组件的参数,就用#defualt="xxxx"
      3. 参数取出来后是一个包含子组件所有通过v-bind传来的参数的对象,记得解构再使用。
      <template>
        <div>
          <button @click="modalRef.openModal()">打开模态框</button>
          <Modal ref="modalRef">
            <template #header">
              <h1>{{ headerName }}</h1>
            </template>
            <template #content="{list}">
              <ul>
                <li v-for="item in list" :key="item.id">
                  {{ item.data }}
                </li>
              </ul>
            </template>
            <template #footer>
              <div>
                <button @click="modalRef.closeModal()">确定</button>
                <button @click="modalRef.closeModal()">取消</button>
              </div>
            </template>
          </Modal>
        </div>
      </template>
      <script setup lang="ts">
      import { ref } from "vue"
      import Modal from "@/components/Modal.vue";
      const modalRef = ref()
      const headerName = ref('我是标题xxx')
      </script>
      
      <style scoped>
      
      </style>
      

      子组件:

      1. slot标签name="header"跟父组件中的#header(v-slot:header)完成对应。
      2. slot标签可以通过v-bind(这里是简写形式)给父组件传参。
      3. slot标签之间可以写默认值,没值的时候就会展示默认值。
      <template>
        <div v-if="isShow">
          <button @click="closeModal">关闭模态框</button>
          <slot name="header">没传值就显示这段默认内容</slot>
          <slot name="content" :list="list"></slot>
          <slot name="footer">没传值就显示这段默认内容</slot>
        </div>
      </template>
      <script setup lang="ts">
      import { ref, reactive, onMounted } from "vue"
      const isShow = ref(false)
      const list = reactive<any>([])
      onMounted(() => {
        // 模拟获取数据
        setTimeout(() => {
          Object.assign(list, [
            { id: 1, data: '1'},
            { id: 2, data: '2'},
            { id: 3, data: '3'}
          ])
          // 模拟处理数据
          list.forEach(item => item.data = Number(item.data))
        }, 1000)
      })
      function openModal(){
        isShow.value = true
      }
      function closeModal(){
        isShow.value = false
      }
      defineExpose({openModal, closeModal})
      </script>
      
      <style scoped>
      
      </style>
      

      拓展 

      想法: 如果另一个父组件需要使用这个模态框,但获取的数据的方式完全不同,原组件就无法复用,因为数据被写死了。

      解决思路:父来获取数据,用props(v-bind)传递给子,子拿到数据只进行逻辑处理,再以参数的形式传回给父,父拿到处理过的数据来控制具体的UI展现形式,这样搞灵活性可以大大增加。

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值