vue3之组件通信

发布于:2025-02-10 ⋅ 阅读:(41) ⋅ 点赞:(0)

vue3之组件通信

Vue3 组件通信和 Vue2 的区别:

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

常见搭配形式:

在这里插入图片描述

1、props 使用

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

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

父组件: Father.vue

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>汽车:{{ car }}</h4>
    <h4 v-show="toy">子给的玩具:{{ toy }}</h4>

    <Child :car="car" :sendToy="getToy" />
  </div>
</template>

<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from 'vue'

let car = ref('奔驰')

let toy = ref('')

function getToy(value: string) {
  toy.value = value
}
</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}
</style>

子组件: Child.vue

<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <h4>父给的车:{{ car }}</h4>
    
    <button @click="sendToy(toy)">把玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
import { ref } from 'vue'
let toy = ref('奥特曼')

// 声明接收 props
defineProps(['car', 'sendToy'])

</script>

<style scoped>
.child {
  background-color: skyblue;
  padding: 10px;
  box-shadow: 0 0 10px black;
  border-radius: 10px;
}
</style>

测试结果:
在这里插入图片描述

2、自定义事件

  1. 概述:自定义事件常用于:子 => 父。
  2. 注意区分好:原生事件、自定义事件。
  • 原生事件:
    • 事件名是特定的(clickmosueenter 等等)
    • 事件对象 $event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:
    • 事件名是任意名称
    • 事件对象 $event: 是调用 emit 时所提供的数据,可以是任意类型!!!

父组件: Father.vue

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4 v-show="toy">子给的玩具:{{ toy }}</h4>

    <button @click="test(1, $event)">点我</button>

    <!-- 给子组件 Child 绑定自定义事件;多个单词官方推荐用肉串命名规则 -->
    <Child @send-toy="saveToy" />
  </div>
</template>

<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";


let toy = ref('')

function saveToy(value: string) {
  console.log('saveToy', value)
  toy.value = value
}

function test(value: number, e: Event) {
  console.log(e) // e 为 PointerEvent 事件对象
}

</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}

.father button {
  margin-right: 5px;
}
</style>

子组件: Child.vue

<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <!-- 2.触发 send-toy 事件 -->
    <button @click="emit('send-toy', toy)">把玩具给父组件</button>
  </div>
</template>

<script setup lang="ts" name="Child">
import { ref } from "vue";

let toy = ref('奥特曼')

// 1.声明事件
const emit = defineEmits(['send-toy'])
</script>

<style scoped>
.child {
  margin-top: 10px;
  background-color: rgb(76, 209, 76);
  padding: 10px;
  box-shadow: 0 0 10px black;
  border-radius: 10px;
}
</style>

测试结果:在这里插入图片描述

3、mitt 使用

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

安装mitt

npm i mitt

引入 emitter: /src/utils/emitter.ts

// 引入 mitt
import mitt from 'mitt'

// 调用 mitt 得到 emitter,emitter能:绑定事件、触发事件
const emitter = mitt()

// 绑定事件
// emitter.on('test1', () => {
//   console.log('test1被调用了')
// })

// 触发事件
// setInterval(() => {
//   emitter.emit('test1')
// }, 1000);

// setTimeout(() => {
//   emitter.off('test1')
//   emitter.all.clear()
// }, 3000);


// 暴露 emitter
export default emitter

父组件: Father.vue

<template>
  <div class="father">
    <h3>父组件</h3>
    <Child1/>
    <Child2/>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Child1 from './Child1.vue'
  import Child2 from './Child2.vue'
</script>

<style scoped>
	.father{
		background-color:rgb(165, 164, 164);
		padding: 20px;
    border-radius: 10px;
	}
  .father button{
    margin-left: 5px;
  }
</style>

子组件1: Child1.vue

<template>
  <div class="child1">
    <h3>子组件1</h3>
    <h4>玩具:{{ toy }}</h4>

    <!-- 触发 -->
    <button @click="emitter.emit('send-toy', toy)">玩具给弟弟(Child2)</button>
  </div>
</template>

<script setup lang="ts" name="Child1">
import { ref } from 'vue'
import emitter from '@/utils/emitter';

let toy = ref('奥特曼')
</script>

<style scoped>
.child1 {
  margin-top: 50px;
  background-color: skyblue;
  padding: 10px;
  box-shadow: 0 0 10px black;
  border-radius: 10px;
}

.child1 button {
  margin-right: 10px;
}
</style>

子组件2: Child1.vue

<template>
  <div class="child2">
    <h3>子组件2</h3>
    <h4>电脑:{{ computer }}</h4>
    <h4>哥哥给的玩具:{{ toy }}</h4>
  </div>
</template>

<script setup lang="ts" name="Child2">
import { ref, onUnmounted } from 'vue'
import emitter from '@/utils/emitter';

let computer = ref('联想')
let toy = ref('')

// 监听:给 emitter 绑定 send-toy 事件
emitter.on('send-toy', (value: any) => {
  toy.value = value
})

// 在组件卸载时解绑 send-toy 事件
onUnmounted(() => {
  emitter.off('send-toy')
})

</script>

<style scoped>
.child2 {
  margin-top: 50px;
  background-color: orange;
  padding: 10px;
  box-shadow: 0 0 10px black;
  border-radius: 10px;
}
</style>

测试结果:
在这里插入图片描述

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">
  1. 组件标签上的 v-model 的本质::moldeValueupdate:modelValue 事件。
 <!-- 组件标签上使用 v-model 指令 -->
<JasonInput v-model="username"/>

<!-- 组件标签上 v-model 的本质 -->
<JasonInput :modelValue="username" @update:modelValue="username = $event"/>

JasonInput 组件

<template>
  <input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)">
</template>

<script setup lang="ts" name="JasonInput">
defineProps(['modelValue'])

const emit = defineEmits(['update:modelValue'])
</script>

<style scoped>
input {
  border: 2px solid black;
  background-image: linear-gradient(45deg, red, yellow, green);
  height: 30px;
  font-size: 20px;
  color: white;
}
</style>

4、也可以更换 value,例如改成 mingmima

# Father.vue
<JasonInput v-model:ming="username" v-model:mima="password" />

# JasonInput 组件
```json

<template>
  <input type="text" :value="ming" @input="emit('update:ming', (<HTMLInputElement>$event.target).value)">
  <br>
  <input type="text" :value="mima" @input="emit('update:mima', (<HTMLInputElement>$event.target).value)">
</template>

<script setup lang="ts" name="JasonInput">
defineProps(['ming', 'mima'])

const emit = defineEmits(['update:ming', 'update:mima'])
</script>

<style scoped>
input {
  border: 2px solid black;
  background-image: linear-gradient(45deg, red, yellow, green);
  height: 30px;
  font-size: 20px;
  color: white;
}
</style>

测试结果:
在这里插入图片描述

5、$attrs 使用

  1. 概述:$attrs 用于实现 当前组件的父组件当前组件的子组件 通信(祖→孙)。
  2. 具体说明:$attrs 是一个对象,包含所有父组件传入的标签属性。

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

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>a:{{ a }}</h4>
    <h4>b:{{ b }}</h4>

    <Child :a="a" :b="b" 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)

function updateA(value: number) {
  a.value += value
}
</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}
</style>

子组件:

<template>
	<div class="child">
		<h3>子组件</h3>
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" name="Child">
	import GrandChild from './GrandChild.vue'
</script>

<style scoped>
	.child{
		margin-top: 20px;
		background-color: skyblue;
		padding: 20px;
		border-radius: 10px;
		box-shadow: 0 0 10px black;
	}
</style>

孙组件:

<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>a:{{ a }}</h4>
    <h4>b:{{ b }}</h4>
    <h4>x:{{ x }}</h4>
    <h4>y:{{ y }}</h4>

    <button @click="updateA(6)">点我将爷爷那的a更新</button> 
  </div>
</template>

<script setup lang="ts" name="GrandChild">
defineProps(['a', 'b', 'x', 'y', 'updateA'])
</script>

<style scoped>
.grand-child {
  margin-top: 20px;
  background-color: orange;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px black;
}
</style>

测试结果:
在这里插入图片描述

6、$$refs 和 $parent 使用

  1. 概述:

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

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

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>房产:{{ house }}</h4>
    <button @click="changeToy">修改Child1的玩具</button>
    <button @click="changeComputer">修改Child2的电脑</button>
    <button @click="getAllChild($refs)">让所有孩子的书变多</button>

    <Child1 ref="c1" />
    <Child2 ref="c2" />
  </div>
</template>

<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref, reactive } from "vue";

let house = ref(4)

// 修改子组件1
function changeToy() {
  c1.value.toy = '小猪佩奇'
}

// 修改子组件2
function changeComputer() {
  c2.value.computer = '华为'
}

function getAllChild(refs: { [key: string]: any }) { // 实在不行就是直接 refs: any
  console.log(refs)

  for (let key in refs) {
    refs[key].book += 3
  }
}
// 向外部提供数据
defineExpose({ house })
</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}

.father button {
  margin-bottom: 10px;
  margin-left: 10px;
}
</style>

子组件1:

<template>
  <div class="child1">
    <h3>子组件1</h3>
    <h4>玩具:{{ toy }}</h4>
    <h4>书籍:{{ book }}</h4>
    <button @click="minusHouse($parent)">干掉父亲的一套房产</button>
  </div>
</template>

<script setup lang="ts" name="Child1">
import { ref } from "vue";

let toy = ref('奥特曼')
let book = ref(3)

// 修改父组件数据
function minusHouse(parent: any) {
  parent.house -= 1
}

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

</script>

<style scoped>
.child1 {
  margin-top: 20px;
  background-color: skyblue;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px black;
}
</style>

子组件2:

<template>
  <div class="child2">
    <h3>子组件2</h3>
    <h4>电脑:{{ computer }}</h4>
    <h4>书籍:{{ book }}</h4>
  </div>
</template>

<script setup lang="ts" name="Child2">
import { ref } from "vue";
let computer = ref('联想')
let book = ref(6)

// 把数据交给外部
defineExpose({ computer, book })
</script>

<style scoped>
.child2 {
  margin-top: 20px;
  background-color: orange;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px black;
}
</style>

测试结果:

在这里插入图片描述

7、provide 和 inject 使用

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

  2. 具体使用:

    • 在祖先组件中通过 provide 配置向后代组件提供数据
    • 在后代组件中通过 inject 配置来声明接收数据

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

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>银子:{{ money }}万元</h4>
    <h4>车子:一辆{{ car.brand }}车,价值{{ car.price }}万元</h4>

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

function updateMoney(value: number) {
  money.value -= value
}

// 向后代提供数据
provide('moneyContext', { money, updateMoney }) // 金额上下文
provide('car', car)

</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}
</style>

子组件:整个组件通信过程都不会修改

<template>
  <div class="child">
    <h3>我是子组件</h3>
    <GrandChild />
  </div>
</template>

<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>

<style scoped>
.child {
  margin-top: 20px;
  background-color: skyblue;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px black;
}
</style>

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

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>银子:{{ money }}</h4>
    <h4>车子:一辆{{ car.brand }}车,价值{{ car.price }}万元</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: (n: number) => { } })
let car = inject('car', { brand: '未知', price: 0 })
</script>

<style scoped>
.grand-child {
  background-color: orange;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px black;
}
</style>

8、pinia

参考之前 pinia 部分的讲解

9、slot 插槽的使用

9.1 默认插槽

Category.vue 组件

<template>
  <div class="category">
    <h2>{{ title }}</h2>
    <!-- 默认插槽 -->
    <slot></slot>
  </div>
</template>

<script setup lang="ts" name="Category">
defineProps(['title'])
</script>

<style scoped>
.category {
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
</style>

Father.vue 组件

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Category title="热门游戏列表">
        <ul>
          <li v-for="g in games" :key="g.id">{{ g.name }}</li>
        </ul>
      </Category>

      <Category title="今日美食城市">
        <img :src="imgUrl" alt="">
      </Category>

      <Category title="今日影视推荐">
        <video :src="videoUrl" controls></video>
      </Category>
    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
import Category from './Category.vue'
import { ref, reactive } from "vue";

let games = reactive([
  { id: 'asgytdfats01', name: '英雄联盟' },
  { id: 'asgytdfats02', name: '王者农药' },
  { id: 'asgytdfats03', name: '红色警戒' },
  { id: 'asgytdfats04', name: '斗罗大陆' }
])
let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')

let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')

</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}

.content {
  display: flex;
  justify-content: space-evenly;
}

img,
video {
  width: 100%;
}
</style>

测试结果:

在这里插入图片描述

9.2 具名插槽

Category.vue 组件

<template>
  <div class="category">
    <!-- s1 插槽 -->
    <slot name="s1"></slot>
    <!-- s2 插槽 -->
    <slot name="s2"></slot>
  </div>
</template>

<script setup lang="ts" name="Category">

</script>

<style scoped>
.category {
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
}
</style>

Father.vue 组件

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">

      <Category>
        <!-- 指定使用 s2 插槽 -->
        <template v-slot:s2>
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </template>

        <template v-slot:s1>
          <h2>热门游戏列表</h2>
        </template>
      </Category>

      <Category>
        <!-- 使用插槽时不会因为放置顺序不同而展示位置发送变化,而是根据组件中定义的插槽顺序展示 -->
        <template v-slot:s2>
          <img :src="imgUrl" alt="">
        </template>

        <template v-slot:s1>
          <h2>今日美食城市</h2>
        </template>
      </Category>

      <Category>
        <!-- 具名插槽简写格式 -->
        <template #s2>
          <video video :src="videoUrl" controls></video>
        </template>

        <template #s1>
          <h2>今日影视推荐</h2>
        </template>
      </Category>
    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
import Category from './Category.vue'
import { ref, reactive } from "vue";

let games = reactive([
  { id: 'asgytdfats01', name: '英雄联盟' },
  { id: 'asgytdfats02', name: '王者农药' },
  { id: 'asgytdfats03', name: '红色警戒' },
  { id: 'asgytdfats04', name: '斗罗大陆' }
])
let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')

let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')

</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}

.content {
  display: flex;
  justify-content: space-evenly;
}

img,
video {
  width: 100%;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
</style>

测试结果:

在这里插入图片描述

9.3 作用域插槽

理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在 News 组件中,但使用数据所遍历出来的结构由 App 组件决定)

Game.vue 组件

<template>
  <div class="game">
    <h2>游戏列表</h2>

    <slot :youxi="games" x="哈哈" y="你好"></slot>
  </div>
</template>

<script setup lang="ts" name="Game">
import { reactive } from 'vue'

let games = reactive([
  { id: 'asgytdfats01', name: '英雄联盟' },
  { id: 'asgytdfats02', name: '王者农药' },
  { id: 'asgytdfats03', name: '红色警戒' },
  { id: 'asgytdfats04', name: '斗罗大陆' }
])
</script>

<style scoped>
.game {
  width: 200px;
  height: 300px;
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}

h2 {
  background-color: orange;
  text-align: center;
  font-size: 20px;
  font-weight: 800;
}
</style>

Father.vue 组件

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Game>
        <!-- params 是插槽传递的所有数据对象 -->
        <template v-slot="params">
          <!-- ul->li -->
          <ul>
            <li v-for="y in params.youxi" :key="y.id">
              {{ y.name }}
            </li>
          </ul>
        </template>
      </Game>

      <Game>
        <template v-slot="params">
          <!-- ol-li -->
          <ol>
            <li v-for="item in params.youxi" :key="item.id">
              {{ item.name }}
            </li>
          </ol>
        </template>
      </Game>

      <Game>
        <!-- 直接从 params 中结构出 youxi -->
        <!-- 作用域插槽也可以指定名字,默认为:default -->
        <template #default="{ youxi }">
          <!-- h3 -->
          <h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
        </template>
      </Game>

    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
import Game from './Game.vue'
</script>

<style scoped>
.father {
  background-color: rgb(165, 164, 164);
  padding: 20px;
  border-radius: 10px;
}

.content {
  display: flex;
  justify-content: space-evenly;
}

img,
video {
  width: 100%;
}
</style>

测试结果:
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到