父组件要通过 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:</h4>
</div>
</template>
<script setup lang="ts" name="Child">
defineProps<{
car: string
}>()
</script>
如果子组件要通过 props 传递信息给父组件,需要先由父组件提供一个用于接收信息的函数:
<template>
<div class="father">
<h3>父组件</h3>
<h4 v-if="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:</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 会被归类为两种:
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
利用 也可以实现组件通信,前文已经说过,这里不再赘述。
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>
本文的完整示例代码可以从获取。
文章评论