[TOC]

组件通信

1. 综述

Vue3组件通信和Vue2的区别:

  • 移出事件总线,使用mitt代替。
  • vuex换成了pinia
  • .sync优化到了v-model里面了。
  • $listeners所有的东西,合并到$attrs中了。
  • $children被砍掉了。

常见搭配形式

2. props

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

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

父组件:

<template>
  <div class="father">
		<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">
		<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>

3. 自定义事件

  1. 概述:自定义事件常用于:子 => 父。

  2. 注意区分好:原生事件、自定义事件。

    • 原生事件:

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

      • 事件名是任意名称

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

      • 命名方式尽量不要驼峰式,而是采取keybab-case式,即send-toy

  3. 示例:

<!-- 自定义事件:在父组件中,给子组件绑定自定义事件 -->
<Child @send-toy="saveToy"/>

<!-- 原生事件:注意区与自定义事件中的$event -->
<button @click="toy = $event">测试</button>
// 父组件中,自定义事件被触发时所调用的函数:
function saveToy(value:string){
  console.log(value)
}
// 子组件中,声明事件并触发:
const emit = defineEmits(['sent-toy'])
emit('send-toy', 具体数据)

4. mitt

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mitt

npm i mitt

新建文件:src\utils\emitter.ts

  • on 触发事件
  • off 移除事件
  • all.clear 移除全部事件

【第一步】:创建mitt文件

// 引入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) // 触发事件
}

5. 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"
    >
    
    • ($event.target)这个是ts的类型断言,target一定是html元素而不为空
    • 数据到页面 :value="userName"
    • 页面到数据 @input="userName =(<HTMLInputElement>$event.target).value"
  3. 组件标签上的v-model的本质::moldeValueupdate:modelValue事件。

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

    在vue3中:

    • 数据到页面 :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">
      defineProps(['modelValue']) // 接收props
      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">
      defineProps(['abc']) // 接收props
      const emit = defineEmits(['update:abc']) // 声明事件
    </script>
    
  5. 如果value可以更换,那么就可以在组件标签上多次使用v-model

    <AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
    
  6. 关于$event到底是什么?什么时候能够.target

    对于原生事件,$event就是事件对象 ====> 能.target

    对于自定义事件,$event就是触发事件时,所传递的对象 ====> 不能.target

6. $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>

7. refs、parent

  1. 概述:

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

    属性 说明
    $refs 值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent 值为对象,当前组件的父组件实例对象。
  3. 子组件需要将数据暴露出来,父组件才能被允许使用;同样的,父组件把需要子组件操作的数据暴露出来,子组件才能拿到使用。

    // 宏函数把数据交给外部
    defineExpose({ toy, book })
    

8. provide、inject

  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>
    

9. slot 插槽

9.1 默认插槽

<!-- 父组件 -->
<template>
  <div id="app">
    <!-- 使用 MyComponent 子组件,并在其中插入内容到默认插槽 -->
    <MyComponent>
      <p>这是传递到默认插槽的内容。</p>
    </MyComponent>
  </div>
</template>
<script setup>
import MyComponent from './MyComponent.vue';
</script>

<!-- 子组件:MyComponent.vue -->
<template>
  <div class="my-component">
    <!-- 这里是默认插槽的位置 -->
    <slot></slot>
  </div>
</template>

9.2 具名插槽

父组件:

<template>
  <div id="app">
    <MyComponent>
      <!-- 使用 v-slot 指令为具名插槽 header 提供内容 -->
      <template v-slot:header>
        <h1>这是头部内容</h1>
      </template>
      <!-- 默认插槽的内容 -->
      <p>这是默认插槽的内容。</p>
			<!-- vue3简写语法: # 来代替 v-slot: -->
      <template #footer>
        <footer>这是底部内容</footer>
      </template>
    </MyComponent>
  </div>
</template>
<script>
import MyComponent from './MyComponent.vue';
</script>

子组件(MyComponent.vue):

<template>
  <div class="my-component">
    <!-- 具名插槽 header -->
    <slot name="header"></slot>
    <!-- 默认插槽 -->
    <slot></slot>
    <!-- 具名插槽 footer -->
    <slot name="footer"></slot>
  </div>
</template>

<script>
export default {
  name: 'MyComponent'
}
</script>

9.3 作用域插槽

作用域插槽(Scoped Slots) 允许子组件向父组件暴露数据,父组件可以使用这些数据来渲染插槽内容。作用域插槽的核心思想是:子组件通过插槽向父组件传递数据,父组件可以基于这些数据动态渲染内容。

1、定义子组件:创建一个子组件 MyList.vue,它会渲染一个列表,并通过作用域插槽将每个列表项的数据传递给父组件。

<template>
  <ul>
    <!-- 使用 v-for 遍历列表 -->
    <li v-for="item in items" :key="item.id">
      <!-- 使用作用域插槽,将 item 数据传递给父组件 -->
      <slot :item="item"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'MyList',
  data() {
    return {
      // 子组件中的数据
      items: [
        { id: 1, name: 'Apple', price: 10 },
        { id: 2, name: 'Banana', price: 5 },
        { id: 3, name: 'Orange', price: 8 },
      ],
    };
  },
};
</script>

<style scoped>
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 10px 0;
  padding: 10px;
  border: 1px solid #ccc;
}
</style>

2、父组件中使用作用域插槽:在父组件中,我们使用子组件 MyList,并通过作用域插槽接收子组件传递的数据,动态渲染内容。

<template>
  <div id="app">
    <h1>商品列表</h1>
    <!-- 使用子组件 MyList -->
    <MyList v-slot="{ item }">
      <!-- 父组件使用子组件传递的数据 item -->
      <div>
        <strong>{{ item.name }}</strong> - 价格: {{ item.price }} 元
      </div>
    </MyList>
  </div>
</template>

<script>
import MyList from './components/MyList.vue';

export default {
  name: 'App',
  components: {
    MyList,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

3、运行结果:运行应用后,页面会渲染如下内容

<div id="app">
  <h1>商品列表</h1>
  <ul>
    <li><div><strong>Apple</strong> - 价格: 10 元</div></li>
    <li><div><strong>Banana</strong> - 价格: 5 元</div></li>
    <li><div><strong>Orange</strong> - 价格: 8 元</div></li>
  </ul>
</div>

4、解释

  1. 子组件 (MyList.vue):
    • 子组件通过 <slot :item="item"></slot>item 数据传递给父组件。
    • item 是子组件中的数据,父组件可以通过作用域插槽访问它。
  2. 父组件 (App.vue):
    • 使用 v-slot="{ item }" 接收子组件传递的数据。
    • item 是子组件暴露的数据,父组件可以基于它动态渲染内容。
  3. 作用域插槽的核心:
    • 子组件通过插槽向父组件传递数据。
    • 父组件可以基于这些数据自定义插槽内容的渲染方式。

9.4 具名作用域插槽

多个具名作用域插槽 允许你在一个组件中定义多个插槽,并且每个插槽都可以向父组件暴露自己的数据。父组件可以根据需要为每个插槽提供内容,并使用子组件传递的数据。

1、定义子组件:创建一个子组件 UserProfile.vue,它包含两个具名作用域插槽:headercontent。每个插槽都会向父组件传递不同的数据。

<template>
  <div class="user-profile">
    <!-- 具名作用域插槽 header,传递用户名称 -->
    <slot name="header" :userName="user.name"></slot>

    <!-- 具名作用域插槽 content,传递用户详细信息 -->
    <slot name="content" :user="user"></slot>
  </div>
</template>

<script>
export default {
  name: 'UserProfile',
  data() {
    return {
      // 子组件中的数据
      user: {
        name: 'John Doe',
        age: 30,
        email: 'john.doe@example.com',
      },
    };
  },
};
</script>

<style scoped>
.user-profile {
  border: 1px solid #ccc;
  padding: 20px;
  margin: 10px;
  max-width: 300px;
}
</style>

2、父组件中使用具名作用域插槽:在父组件中,我们使用子组件 UserProfile,并为每个具名作用域插槽提供内容。父组件可以访问子组件传递的数据,并基于这些数据渲染内容。

<template>
  <div id="app">
    <h1>用户信息</h1>
    <!-- 使用子组件 UserProfile -->
    <UserProfile>
      <!-- 为具名插槽 header 提供内容,接收 userName -->
      <template v-slot:header="{ userName }"><!-- 简写:<template #header="{ userName }"> -->
        <h2>欢迎, {{ userName }}!</h2>
      </template>

      <!-- 为具名插槽 content 提供内容,接收 user,简写:<template #content="{ user }"> -->
      <template v-slot:content="{ user }">
        <div>
          <p><strong>年龄:</strong> {{ user.age }}</p>
          <p><strong>邮箱:</strong> {{ user.email }}</p>
        </div>
      </template>
    </UserProfile>
  </div>
</template>

<script>
import UserProfile from './components/UserProfile.vue';

export default {
  name: 'App',
  components: {
    UserProfile,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

3、运行结果

<div id="app">
  <h1>用户信息</h1>
  <div class="user-profile">
    <h2>欢迎, John Doe!</h2>
    <div>
      <p><strong>年龄:</strong> 30</p>
      <p><strong>邮箱:</strong> john.doe@example.com</p>
    </div>
  </div>
</div>

4、解释

  1. 子组件 (UserProfile.vue):
    • 定义了两个具名作用域插槽:headercontent
    • header 插槽传递了 userName 数据。
    • content 插槽传递了整个 user 对象。
  2. 父组件 (App.vue):
    • 使用 v-slot:header 接收 header 插槽传递的 userName 数据,并渲染欢迎消息。
    • 使用 v-slot:content 接收 content 插槽传递的 user 数据,并渲染用户的详细信息。
  3. 多个具名作用域插槽的优势:
    • 允许子组件将不同的数据暴露给父组件。
    • 父组件可以灵活地为每个插槽提供内容,并根据子组件传递的数据动态渲染。
Last Updated: 2/11/2025, 11:18:29 AM