组件事件监听

在编写博客内容的时候,有时候发现它需要能够和父组件交互,比如想要将博客文章内容文字放大,但是其他部分的仍旧使用默认的字号。

在父组件中可以添加一个postFontSize ref来实现这个效果:

const posts = ref([
/* ... */
])

const postFontSize = ref(1)

在模板中使用它来控制所有博客文章的字体大小:

<div :style="{ fontSize: postFontSize + 'em'}">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>

<BlogPost>组件添加一个按钮:

<!-- BlogPost.vue -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>

这个按钮现在没有做任何事情,在点击这个按钮的时候来告诉父组件,应该放大所有博客文章的文字。父组件可以通过v-on@来选择性地监听子组件上抛的事件,就像监听原生DOM事件那样:

<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>

子组件可以通过内置的$emit方法,通过传入事件名称来抛出一个事件:

<!-- BlogPost.vue -->
<tempalte>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>

因为有了@enlarge-text="postFontSize += 0.1"的监听,父组件会接收这一事件,从而更新postFontSize的值。

我们可以通过defineEmits宏来声明需要抛出的事件:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

声明了一个组件可能触发的所有事件,同时还可以让Vue避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

defineProps类似,defineEmits仅可用于<script setup>之中,并不需要导入,它返回一个等同于$emit方法的emit函数。它可以被用于在组件的<script setup>中抛出事件,因为此处无法直接访问$emit

<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

如果没有使用<script setup>,可以通过emits选项定义组件会抛出的事件。可以从setup()函数的第二个参数,即setup上下文对象上访问到的emit函数:

export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}

总结

在组件的模板表达式中,可以直接使用$emit方法触发自定义事件:

<!-- MyComponent -->
<button @click="emit('someEvent')">Click Me</button>

父组件可以通过v-on缩写为@来监听事件:

<MyComponent @some-event="callback" />

同样,组件的事件监听器也支持.once修饰符:

<MyComponent @some-event.once="callback" />

像组件与prop一样,事件的名字也提供了自动的格式转换。注意这里触发了一个以camelCase形式命名的事件,但在父组件中可以使用kebab-case形式来监听。与prop大小写格式一样,在模板中也推荐使用kebab-case形式来编写监听器。

和原生DOM事件不一样,组件触发的事件没有冒泡机制。只能监听直接子组件触发的事件。平级组件或者跨越多层嵌套的组件间通信,应该使用一个外部的事件总线,或者使用一个全局状态管理。

事件参数

有时候需要在触发事件时附带一个特定的值。举个例子,想要在<BlogPost>组件来管理文本会缩放多大,这个场景下,可以给$emit提供一个额外的参数:

<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>

然后在父组件中监听事件,可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:

<MyButton @increase-by="(n) => count += n" />

也可以使用组件方法的来作为事件处理函数:

<MyButton @increase-by="increaseCount" />

该方法会接收到事件传递的参数:

function increaseCount(n) {
count.value += n
}

所有传入$emit()的额外参数都会被直接传向监听器。如:$emit('foo', 1, 2, 3)触发后,监听器函数将会受到这三个参数值。

声明触发的事件

<template>中使用的$emit方法不能在组件的<script setup>部分中使用,应该通过defineEmits()返回一个相同作用的函数来使用。

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
emit('submit')
}
</script>

defineEmits不能在子函数中使用。必须直接放置在<script setup>的顶级作用域下。

如果是显式地使用了setup函数而不是<script setup>,则事件需要通过emits选项定义,emit函数暴露在setup()上下文对象中:

export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}

setup()上下文对象中的其他属性一样,emit可以被安全的解构:

export default {
emits: ['inFocus', 'submit'],
setup(props, {emit}) {
emit('submit')
}
}

这个emits选项和defineEmits()宏还支持对象语法。通过Typescript为参数指定类型,它允许对触发事件的参数进行校验:

<script setup lang="ts">
const emit = defineEmits({
submit(payload: {email: string, password: string}) {
//通过返回值为`true`还是为`false`来判断验证是否通过
}
})
</script>

如果搭配Typescript使用<script setup>,可以使用纯类型标注来声明触发的事件:

<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>

尽管事件声明是可选的,但是还是推荐完整地声明所有要触发的事件,这样可以在代码中记录组件的用法。同时事件声明可以让Vue更好地将事件和透传attribute作出区分,从而避免一些由第三方代码触发的自定义DOM事件所导致的边界情况。

如果一个原生事件的名字,如(click)被定义在了emits选项中,那么监听器只会监听组件触发的click事件,而不会再响应原生的click事件。

事件校验

和对props添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。

要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入emit的内容,返回一个布尔值来表明事件是否合法。

<script setup>
const emit = defineEmits({
//没有校验
click: null,

//校验submit事件
submit: ({email, password}) => {
if (email && password) {
return true
}else {
console.warn('Invalid submit event payload!')
return false
}
}
})

function submitForm(email, password) {
emit('submit', {email, password})
}
</script>