红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

Vue3 学习笔记 7:组件通信

2025年10月2日 30点热度 0人点赞 0条评论

props

父组件要通过 props 传递信息给子组件,只要在子组件上添加相应的属性:

<template>
    <div class="father">
        <h3>父组件</h3>
        <Child :car="car"/>
    </div>
</template>
​
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from 'vue'
const car = ref('宝马')
</script>

子组件通过defineProps函数进行接收:

<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>父组件的 car:{{ car }}</h4>
  </div>
</template>
​
<script setup lang="ts" name="Child">
defineProps<{
    car: string
}>()
</script>

如果子组件要通过 props 传递信息给父组件,需要先由父组件提供一个用于接收信息的函数:

<template>
    <div class="father">
        <h3>父组件</h3>
        <h4 v-if="childToy">子组件的玩具:{{ childToy }}</h4>
        <Child :car="car" :sendToy="getToy" />
    </div>
</template>
​
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from 'vue'
const car = ref('宝马')
const childToy = ref('')
function getToy(toy: string) {
    childToy.value = toy
}
</script>

子组件接收并调用该方法传递信息给父组件:

<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>父组件的 car:{{ car }}</h4>
    <button @click="sendToy(toy)">给父组件发送玩具</button>
  </div>
</template>
​
<script setup lang="ts" name="Child">
import { ref } from 'vue'
defineProps<{
    car: string
    sendToy: Function
}>()
const toy = ref('奥特曼')
</script>

自定义事件

除了通过 props 传递函数给子组件实现子组件向父组件通信外,还可以通过自定义事件实现。

首先在父组件中为子组件绑定一个自定义事件:

<template>
    <div class="father">
        <h3>父组件</h3>
        <Child @send-toy="getToy"/>
    </div>
</template>
​
<script setup lang="ts" name="Father">
import Child from './Child.vue'
function getToy(toy: string) {
    console.log(toy)
}
</script>

自定义事件名为send-toy,事件发生时会触发父组件的getToy方法,该方法接收一个参数,用于子组件向父组件发送信息。

Vue 官方推荐使用肉串命名(连字符-连接单词)的方式命名自定义事件名称。

在子组件中通过宏函数defineEmits定义事件,方式类似于defineProps:

const emit = defineEmits(['send-toy'])

习惯上返回值被命名为emit。

定义返回值是为了之后能够调用这个自定义函数。

为子组件添加一个按钮,按钮点击后触发事件并向父组件发送信息:

<button @click="emit('send-toy', '奥特曼')">发送小玩具</button>

通过emit触发事件时,第一个参数是自定义事件名称,之后的参数是为事件绑定方法传递的参数。

mitt

mitt 是一个第三方组件,可以通过它实现任意组件的通信。

其原理是 mitt 可以充当一个第三方工具,借助它绑定事件/触发事件就可以让任意组件进行通信。

需要先安装 mitt:

npm install mitt

添加文件src\utils\emitter.ts:

import mitt from 'mitt'

export const emitter = mitt()

在需要接收消息的组件中通过emitter绑定事件:

<template>
	<div class="child1">
		<h3>子组件1</h3>
	</div>
</template>

<script setup lang="ts" name="Child1">
import { emitter } from '@/utils/emitter'
emitter.on('send-toy', (value: any) => {
	console.log('子组件1接收 toys:', value)
})
</script>

在需要发送消息的组件中通过emitter发送事件:

<template>
  <div class="child2">
    <h3>子组件2</h3>
	<h4>玩具:{{ toy }}</h4>
	<button @click="emitter.emit('send-toy', toy)">发送玩具</button>
  </div>
</template>

<script setup lang="ts" name="Child2">
import { ref } from 'vue'
import { emitter } from '@/utils/emitter';
const toy = ref('奥特曼')
</script>

最好在组件卸载时将绑定到 emitter 的事件解绑:

<script setup lang="ts" name="Child1">
import { emitter } from '@/utils/emitter'
import { onUnmounted } from 'vue';
emitter.on('send-toy', (value: any) => {
	console.log('子组件1接收 toys:', value)
})
onUnmounted(() => {
	emitter.off('send-toy')
})
</script>

这样可以避免组件已经被移除,但emitter上还存在无效的绑定事件(一种内存泄露)。

mitt 的主要功能:

  • emitter.on,绑定事件

  • emitter.emit,触发事件

  • emitter.off,解绑事件

  • emitter.all.clear,解绑所有事件

v-model

添加一个组件:

<template>
    <input></input>
</template>
<script setup lang="ts" name="MyInput">
</script>

在父组件中使用这个组件:

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

<script setup lang="ts" name="Father">
import MyInput from "./MyInput.vue"
import { ref } from "vue"
const msg = ref('hello')
</script>

如果要传递一个响应式数据给子组件,且需要在子组件的数据发生变化时将改变后的数据回写,就需要:

    <MyInput :modelValue="msg" @update:modelValue="msg = $event"/>

这里为子组件添加了一个名为modelValue的 props,以及自定义事件update:modelValue。props 用于传递数据给子组件,自定义事件用于子组件数据发生变化时回写数据(通过触发事件)。

在子组件中需要定义 props 和事件:

<script setup lang="ts" name="MyInput">
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

为了 input 控件展示数据,需要进行单向数据绑定。此外还需要在input组件有数据输入时,调用input事件,并调用update:modelValue事件将修改后的值传递给父组件:

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

标准 HTML 标签的标准事件中,$event是事件对象,可以通过$event.target.value获取事件触发时的值,而自定义组件中的自定义事件中,$event是事件触发一方传递的信息(这个例子中是修改后的值)。

这样就可以实现自定义组件的数据双向绑定,实际上父组件可以使用简写方式:

    <MyInput v-model="msg"/>

这个例子实际上就是自定义组件库通过v-model实现数据双向绑定的原理。通常不会通过这种方式传递信息,这种方式通常用于封装组件库。

自定义 v-model 名称

上面展示了 v-model 默认实现使用的 props 和 自定义事件命名,实际上这个名称是可以修改的。

比如如果要向子组件绑定多个 v-model 数据:

    <MyInput v-model:username="username" v-model:password="password"/>

对应的,子组件中需要修改为:

<template>
    <input :value="username" @input="emit('update:username', (<HTMLInputElement>$event.target).value)" />
    <br/>
    <input :value="password" @input="emit('update:password', (<HTMLInputElement>$event.target).value)" />
</template>
<script setup lang="ts" name="MyInput">
defineProps(['username', 'password'])
const emit = defineEmits(['update:username', 'update:password'])
</script>

$attrs

通过 props 传递信息时,子组件可以只接收部分 props:

<template>
  <div class="father">
    <h3>父组件</h3>
		<Child :a="a" :b="b" :c="c"/>
  </div>
</template>

<script setup lang="ts" name="Father">
import Child from './Child.vue'
import {ref} from 'vue'
const a = ref(1)
const b = ref(2)
const c = ref(3)
</script>
<template>
	<div class="child">
		<h3>子组件</h3>
		<h4>a: {{ a }}</h4>
		<GrandChild/>
	</div>
</template>

<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
defineProps(["a"])
</script>

此时子组件中由父组件传递的 props 会被归类为两种:

image-20250921165144758

props中的数据是由defineProps定义接收的,可以在子组件的模版中直接使用的数据,attrs中的数据则是剩下的数据。

如果要在子组件中使用 attrs 中的数据,可以:

<h4>attrs: {{ $attrs }}</h4>

给组件添加 props 属性,除了使用常见的:xxx='xxx',以外,还可以通过v-bind='{...}'将对象的属性添加为 props:

<Child :a="a" :b="b" :c="c" v-bind="{ x: 100, y: 200 }" />

本质上等价于:

<Child :a="a" :b="b" :c="c" :x="100" :y="200" />

因此可以通过以下方式直接在父组件中将爷爷组件传递的 props 传递给孙组件:

<GrandChild v-bind="$attrs"/>

在孙组件中接收:

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

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

如果孙组件需要传递数据给爷爷组件,可以使用爷爷组件传递的方法,方法同样可以借助$attrs传递给孙组件:

<Child :a="a" :b="b" :c="c" :x="100" :y="200" :sendMsg="getMsg"/>
<template>
	<div class="grand-child">
		<h3>孙组件</h3>
		<p>a: {{a}}</p>
		<p>b: {{b}}</p>
		<p>c: {{c}}</p>
		<p>x: {{x}}</p>
		<p>y: {{y}}</p>
		<button @click="sendMsg('hello')">发送信息</button>
	</div>
</template>

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

$ refs 与 $parent

之前提到过,利用ref可以获取当前使用的组件实例,进而使用组件实例的数据或方法:

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

<script setup lang="ts" name="Child1">
import { ref } from 'vue'
const toy = ref('小鸭子')
const bookNum = ref(0)
defineExpose({
	toy, bookNum
})
</script>
<template>
	<div class="father">
		<h3>父组件</h3>
		<button @click="changeC1Toy">修改儿子1的玩具</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 } from 'vue'
const c1 = ref()
const changeC1Toy = () => {
	c1.value.toy = '变形金刚'
}

</script>

注意,只能使用目标组件通过defineExpose暴露的数据或方法。

这实际上也是一种组件通信的方式。

特别的,可以通过特殊变量$refs获取所有使用ref标记的组件实例:

<template>
	<div class="father">
		<h3>父组件</h3>
		<button @click="changeC1Toy">修改儿子1的玩具</button>
		<button @click="changeAllChildrenBookNum($refs)">修改所有儿子的书本数目</button>
		<h4>{{ drinkNum }} 瓶酒</h4>
		<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 } from 'vue'
const drinkNum = ref(100)
const c1 = ref()
const changeC1Toy = () => {
	c1.value.toy = '变形金刚'
}
const changeAllChildrenBookNum = (refs: {[key:string]:any}) => {
	for (let key in refs) {
		refs[key].bookNum += 1
	}
}
defineExpose({
	drinkNum
})
</script>

这里的refs: {[key:string]:any}表示refs是一个属性名为string类型值为任意类型的对象。

类似的,还有一个特殊变量$parent,可以在模版中获取父组件的实例,利用它同样可以修改父组件的数据:

<template>
	<div class="child2">
		<h3>子组件2</h3>
		<h4>{{ bookNum }}本书</h4>
		<button @click="drink($parent)">喝父亲的酒</button>
	</div>
</template>

<script setup lang="ts" name="Child2">
import { ref } from 'vue'
const bookNum = ref(0)
const drink = (parent: any) => {
	parent.drinkNum--
}

defineExpose({
	bookNum
})
</script>

provide 与 inject

虽然借助$attrs可以完成祖孙通信,但需要父组件参与,使用provide与inject可以实现不需要中间组件参与的祖孙通信。

使用provide可以将数据(或方法)提供给后代组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>钱:{{ money }}</h4>
    <h4>车:{{ car }}</h4>
    <Child />
  </div>
</template>

<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref, reactive, provide } from 'vue'
const money = ref(1000)
const car = reactive({
  brand: '奔驰',
  price: 5000
})
// 将数据提供给后代组件
provide('money', money)
provide('car', car)
</script>

在子组件中使用inject可以获取数据:

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>钱:{{ money }}</h4>
    <h4>车:{{ car }}</h4>
  </div>
</template>

<script setup lang="ts" name="GrandChild">
import { inject } from 'vue';
const money = inject('money')
const car = inject('car')
</script>

如果需要后代组件给祖先组件传递信息,可以通过provide提供方法:

const changeMoney = (moneyDiff: number) => {
  money.value += moneyDiff
}
// 将数据提供给后代组件
provide('moneyContext', { money, changeMoney })

后代组件获取并调用方法:

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>钱:{{ money }}</h4>
    <h4>车:{{ car }}</h4>
    <button @click="changeMoney(-100)">-100</button>
  </div>
</template>

<script setup lang="ts" name="GrandChild">
import { inject } from 'vue';
const { money, changeMoney } = inject<any>('moneyContext')
const car = inject('car')
</script>

Pinia

利用 Pinia 也可以实现组件通信,前文已经说过,这里不再赘述。

Slot

如果父组件要把一段 HTML 片段传递给子组件,可以使用插槽(Slot)。

默认插槽

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Category title="热门游戏列表">
        <ul v-for="item in games" :key="item.id">
          <li>{{ item.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://ts1.tc.mm.bing.net/th/id/R-C.939630a19cb4bd26d3ef8f81b1c4a96b?rik=ZaGrZV4qhsqQfg&riu=http%3a%2f%2fn.sinaimg.cn%2fsinacn10%2f213%2fw2048h1365%2f20180324%2fa906-fysnevm4424507.jpg&ehk=DxenTApAqLcUhUCviEQqCiGBL8pXqcWHXRQJD%2fTp7rQ%3d&risl=&pid=ImgRaw&r=0')
let videoUrl = ref('https://www.w3schools.com/html/mov_bbb.mp4')
</script>

子组件:

<template>
    <div class="category">
      <h2>{{title}}</h2>
      <slot></slot>
    </div>
</template>
<script setup lang="ts" name="Category">
  defineProps(['title'])
</script>

父组件通过子组件标签体传递 HTML 片段给子组件,子组件通过<slot>标签呈现父组件传递的内容。这种方式被称作“默认插槽”。

具名插槽

可以为插槽指定名称,这种插槽被称为“具名插槽”。

其实默认插槽也有名称,是default。

子组件中的插槽:

<slot name="content"></slot>

此时父组件中就需要指定使用哪个插槽:

<Category v-slot:content title="热门游戏列表">
    <ul v-for="item in games" :key="item.id">
        <li>{{ item.name }}</li>
    </ul>
</Category>

这里v-slot:content表示使用子组件名称为content的插槽。

这里有个简写形式,v-slot:content可以简写为#content:

<Category #content title="热门游戏列表">

一般来说,子组件中只有一个插槽这么做没有意义(使用默认插槽即可),所以更常见的是在子组件有多个插槽时指定每个插槽的名称:

<template>
    <div class="category">
      <slot name="title">默认名称</slot>
      <slot name="content">默认内容</slot>
    </div>
</template>

slot可以指定默认内容,如果父组件没有传递就使用默认内容。

父组件:

<Category>
    <template #content>
<ul v-for="item in games" :key="item.id">
    <li>{{ item.name }}</li>
        </ul>
    </template>
    <template #title>
<h2>热门游戏列表</h2>
    </template>
</Category>

如果有多个插槽,就不能在组件标签中指定 HTML 片段对应的插槽,而是要将相应的 HTML 片段用template标签包裹,在 template 标签上指定对应的插槽。

作用域插槽

如果父组件要在通过插槽传递的 HTML 片段中使用子组件中的数据,就需要使用作用域插槽。

子组件:

<template>
  <div class="game">
    <h2>{{ title }}</h2>
    <slot :games="games" :title="title"></slot>
  </div>
</template>

<script setup lang="ts" name="Game">
import { reactive, ref } from 'vue'
let games = reactive([
  { id: 'asgytdfats01', name: '英雄联盟' },
  { id: 'asgytdfats02', name: '王者农药' },
  { id: 'asgytdfats03', name: '红色警戒' },
  { id: 'asgytdfats04', name: '斗罗大陆' }
])
let title = ref('游戏列表')
</script>

子组件中有一个默认插槽,该插槽通过:games="games" :title="title"的方式将子组件的数据暴露给插槽使用者(这里是父组件)。

父组件使用子组件暴露的数据填充插槽的 HTML 片段:

<Game>
    <template v-slot="scope">
        <ul>
            <li v-for="g in scope.games" :key="g.id">
                {{ g.name }}
            </li>
        </ul>
    </template>
</Game>

插槽暴露的数据以v-slot="scope"的方式接收,这里可以使用任意名称,但习惯上使用scope命名,因此叫做作用域插槽。

当然也可以用插槽的简写方式以及解构赋值进行简化,比如:

<Game>
    <template #default="{games}">
        <h3 v-for="g in games" :key="g.id">{{ g.name }}</h3>
    </template>
</Game>

本文的完整示例代码可以从这里获取。

参考资料

  • 尚硅谷Vue3入门到实战

  • developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub.

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: vue 组件通信
最后更新:2025年10月2日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号