文章目录
总结
- DOM原生事件
- 父组件的原生事件会透传(fall through)到子组件的根节点
- 透传的方向是从父到子
- 对于多根节点的子组件,则默认不会透传
- 子组件可以设置
inheritAttrs来控制是否允许透传 - 执行顺序:默认按照事件冒泡机制的顺序(从里向外)
- 自定义事件
- 子组件可以向父组件emit自定义事件
- emit的方向是从子到父
- 通过
$emit()方法向父组件发送自定义事件- 第一个参数是事件名,推荐使用
kebab-case - 后面的参数都作为事件的参数
- 第一个参数是事件名,推荐使用
- 子组件可以通过
emits选项显式声明自定义事件(推荐) - 对于显式声明的事件,不会把父组件的同名事件透传过来(不过最好还是别同名,比如别使用原生事件名)
- 在
emits显式声明里,可以加上校验的逻辑
环境
- Ubuntu 24.04
- Chrome Version 146.0.7680.164 (Official Build) (64-bit)
- VSCode 1.109.5
- npm 11.6.2
- Vue 3.5.32
准备
创建一个Vue项目。创建过程略,具体可参见 https://blog.csdn.net/duke_ding2/article/details/159510007 。
关于如何导入和注册组件,参见 https://blog.csdn.net/duke_ding2/article/details/159472143 。
关于 props ,参见 https://blog.csdn.net/duke_ding2/article/details/159696940 。
创建文件 MyComponent.vue 如下:
html
<template>
<div>
<h1>Here is title</h1>
<p style="color: blue;font-size: 30px;">Here is content</p>
</div>
</template>
在 App.vue 中使用该组件:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
}
}
</script>
<template>
<MyComponent />
</template>
效果如下:

事件
组件可能需要响应事件,比如,在点击标题的时候,需要做一些事情。
原生事件
子组件的原生事件
修改 MyComponent.vue 如下:
html
<script>
export default {
methods: {
titleClick() {
console.log('title clicked')
}
},
}
</script>
<template>
<div>
<h1 @click="titleClick">Here is title</h1>
<p style="color: blue;font-size: 30px;">Here is content</p>
</div>
</template>
点击标题,效果如下:

这是一个标准的DOM原生事件。
父组件的原生事件(透传)
你可能会想,如果在父组件里直接定义 @click 事件,会怎么样?
把 MyComponent.vue 还原,然后修改 App.vue 如下:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
},
methods: {
myComponentClick() {
console.log('myComponent clicked')
}
},
}
</script>
<template>
<MyComponent @click="myComponentClick"/>
</template>
点击标题,效果如下:

当然,采用这种做法,不只是点击标题会触发事件,点击内容也会触发事件,因为事件是作用在整个子组件上的。
也就是说,事件是从父组件"透传(fall through)"到了子组件,作用到了子组件的根节点上。本例中,子组件的根节点是 <div> 元素。无论点击标题还是内容,由于事件冒泡机制,事件都会冒泡到根元素。
注:本文只讨论单根节点。事实上,Vue 3支持多根节点。对于多根节点,父组件事件默认不会透传到子组件(可能是因为无法确定作用到哪个根元素吧)。子组件必须显式绑定 $attrs 才行。
另外,子组件可以通过 inheritAttrs 来设置是否允许透传。
修改 MyComponent.vue 如下:
html
<script>
export default {
inheritAttrs: false, // 禁止属性透传到根元素
methods: {
titleClick() {
console.log('title clicked')
}
},
}
</script>
<template>
<div>
<h1 @click="titleClick">Here is title</h1>
<p style="color: blue;font-size: 30px;">Here is content</p>
</div>
</template>
点击标题,效果如下:

可见,设置 inheritAttrs 之后,父组件事件没有透传到子组件。
原生事件冒泡
那么问题来了,如果在父组件和子组件里同时定义了 @click ,会怎么样?
修改 MyComponent.vue ,删除 inheritAttrs 。
点击标题,效果如下:

可见,父组件和子组件的原生事件都被触发了,顺序是先子后父,也就是冒泡的顺序。
自定义事件(emit)
有时候,父组件在使用子组件时,可能需要定制化处理子组件的事件。换句话说,子组件负责"触发"事件,而父组件负责"处理"事件。比如,点击标题和内容,会触发两个不同的事件,而父组件负责处理这些事件。
在子组件里,可通过 $emit() 方法来向父组件发送自定义事件。
修改 MyComponent.vue 如下:
html
<script>
export default {
methods: {
titleClick() {
console.log('title clicked')
this.$emit('title-click')
},
contentClick() {
console.log('content clicked')
this.$emit('content-click')
}
}
}
</script>
<template>
<div>
<h1 @click="titleClick">Here is title</h1>
<p @click="contentClick" style="color: blue;font-size: 30px;">Here is content</p>
</div>
</template>
本例中,在子组件里,点击标题或者内容,会触发相应的事件,然后在事件里通过 $emits() 方法,向父组件发送一个自定义事件。
修改 App.vue 如下:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
},
methods: {
handleTitleClick() {
console.log('parent: title clicked')
},
handleContentClick() {
console.log('parent: content clicked')
}
},
}
</script>
<template>
<MyComponent @title-click="handleTitleClick" @content-click="handleContentClick"/>
</template>
在父组件里,定义了 title-click 和 content-click 两个事件,这不是HTML原生事件,而是从子组件emit过来的自定义事件。
参数
$emit() 方法可以有多个参数:
javascript
$emit: (event: string, ...args: any[])
第一个参数是自定义的方法名,是字符串类型,比如本例中的 title-click 。
这里涉及到了方法名的转换问题。
常见的字符串格式如下:
kebab-case,比如:how-are-youcamelCase,比如:howAreYouPascalCase,比如:HowAreYou
"子组件在 $emit() 方法里指定的事件名"和"父组件里实际的事件名"在匹配时,遵循如下规则:
- 大小写敏感,比如
titleClick和titleclick不匹配 - 首字母大小写不敏感,比如
titleClick和TitleClick匹配 - 大写字母和"连字符加小写字母"匹配,比如
titleClick和title-click匹配
也就是说,对于同一个字符串来说,其 kebab-case , camelCase , PascalCase 格式是相互匹配的。
本例中,第一个参数如果是 titleClick 、 TitleClick 、 title-click ,则父组件也可以用这几个中的任何一个。
Vue推荐使用 kebab-case 。
$emit() 方法从第二个参数开始,都是自定义参数,会原封不动的传给父组件,作为自定义方法的参数。
比如,修改 MyComponent.vue 如下:
html
<script>
export default {
methods: {
titleClick() {
console.log('title clicked')
this.$emit('title-click', 123, 'abc') // 触发事件,并传递参数
},
contentClick() {
console.log('content clicked')
this.$emit('content-click')
}
}
}
</script>
<template>
<div>
<h1 @click="titleClick">Here is title</h1>
<p @click="contentClick" style="color: blue;font-size: 30px;">Here is content</p>
</div>
</template>
修改 App.vue 如下:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
},
methods: {
handleTitleClick(arg1, arg2) { // 接收事件参数
console.log('parent: title clicked: ', arg1, arg2)
},
handleContentClick() {
console.log('parent: content clicked')
}
},
}
</script>
<template>
<MyComponent @title-click="handleTitleClick" @content-click="handleContentClick"/>
</template>
点击标题,效果如下:

下面来看一个更有意义的例子。
修改 MyComponent.vue 如下:
html
<script>
export default {
props: {
id: Number,
title: String,
content: String,
},
methods: {
titleClick() {
this.$emit('title-click', this.id) // 把id传给父组件
}
},
}
</script>
<template>
<div>
<h1 @click="titleClick">{{ title }}</h1>
<p style="color: blue;font-size: 30px;">{{ content }}</p>
</div>
</template>
修改 App.vue 如下:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
},
data() {
return {
arr1: [
{id: 1, title: '静夜思', content: '床前明月光...', remark: '作者:李白'},
{id: 2, title: '春晓', content: '春眠不觉晓...', remark: '作者:孟浩然'},
]
}
},
methods: {
handleTitleClick(id) {
const item = this.arr1.find(item => item.id === id) // 通过子组件传递过来的id,查找对应的子组件
if (item) {
console.log(`${item.title},${item.remark}`)
} else {
console.log(`未找到对应的项: ${id}`)
}
}
}
}
</script>
<template>
<div>
<MyComponent v-for="item in arr1" @title-click="handleTitleClick" :key="item.id" :id="item.id" :title="item.title" :content="item.content" />
</div>
</template>
点击标题,效果如下:

父组件在 v-for 里使用了子组件,所以子组件在emit title-click 事件时,加上了 id 参数,以便父组件查找是哪个子组件触发的事件。
注意:本例中,父组件并没有把 remark 传给子组件,因为子组件不需要处理 remark 数据(当然,为了方便,也可以把 item 作为一个整体传给子组件)。
显式声明
在子组件里,可以通过 emits 选项显式声明所emit的自定义事件。
修改 MyComponent.vue 如下:
html
<script>
export default {
props: {
id: Number,
title: String,
content: String,
},
emits: ['titleClick'], // 显式声明
methods: {
titleClick() {
this.$emit('titleClick', this.id)
}
},
}
</script>
<template>
<div>
<h1 @click="titleClick">{{ title }}</h1>
<p style="color: blue;font-size: 30px;">{{ content }}</p>
</div>
</template>
Vue推荐使用显式声明。原因有二:
- 一目了然,知道子组件会emit哪些自定义事件
- 更重要的原因是,避免原生的事件在父组件中被触发两次
前面提到,父组件的原生事件会透传到子组件。那么问题来了,如果子组件emit的自定义事件,其事件名是一个原生事件(比如 click ),会怎么样呢?
修改 MyComponent.vue 如下:
html
<script>
export default {
props: {
id: Number,
title: String,
content: String,
},
// emits: ['click'], // 不显式声明
methods: {
titleClick() {
this.$emit('click', this.id) // 'click' 是原生事件
}
},
}
</script>
<template>
<div>
<h1 @click="titleClick">{{ title }}</h1>
<p style="color: blue;font-size: 30px;">{{ content }}</p>
</div>
</template>
修改 App.vue 如下:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
},
data() {
return {
arr1: [
{id: 1, title: '静夜思', content: '床前明月光...', remark: '作者:李白'},
{id: 2, title: '春晓', content: '春眠不觉晓...', remark: '作者:孟浩然'},
]
}
},
methods: {
handleTitleClick(id) {
const item = this.arr1.find(item => item.id === id) // 通过子组件传递过来的id,查找对应的子组件
if (item) {
console.log(`${item.title},${item.remark}`)
} else {
console.log(`未找到对应的项: ${id}`)
}
}
}
}
</script>
<template>
<div>
<MyComponent v-for="item in arr1" @click="handleTitleClick" :key="item.id" :id="item.id" :title="item.title" :content="item.content" />
</div>
</template>
刷新页面,点击标题,效果如下:

可见,父组件的 @click 事件被触发了两次,一次是子组件emit过来的,另一次是父组件本身触发的(由于没有 id 参数,所以报错了)。
修改 MyComponent.vue ,给子组件加上显式 emits :
javascript
emits: ['click'],
点击标题,效果如下:

可见,这次父组件的 @click 事件只被触发了一次。这是因为,子组件通过 emits 显式声明,告诉Vue" click 是我自定义的事件",只作为组件事件处理,不会把父组件的原生事件透传到子组件根元素。
总结:
- 最好加上
emits显式声明 - emit自定义事件时,最好不要使用原生的事件名
事件校验
在 emits 显式声明里,可以加上校验的逻辑。
修改 MyComponent.vue 如下:
html
<script>
export default {
props: {
id: Number,
title: String,
content: String,
},
emits: {
"title-click": (id) => { // 声明事件,并验证传递的参数
return id > 5 // 假定 id 必须大于 5
},
"content-click": null // 声明事件,不验证参数
},
methods: {
titleClick() {
this.$emit('title-click', this.id)
},
contentClick() {
this.$emit('content-click', this.id)
}
},
}
</script>
<template>
<div>
<h1 @click="titleClick">{{ title }}</h1>
<p @click="contentClick" style="color: blue;font-size: 30px;">{{ content }}</p>
</div>
</template>
修改 App.vue 如下:
html
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 同名简写
},
data() {
return {
arr1: [
{id: 1, title: '静夜思', content: '床前明月光...', remark: '作者:李白'},
{id: 2, title: '春晓', content: '春眠不觉晓...', remark: '作者:孟浩然'},
]
}
},
methods: {
handleTitleClick(id) {
const item = this.arr1.find(item => item.id === id) // 通过子组件传递过来的id,查找对应的子组件
if (item) {
console.log(`${item.title},${item.remark}`)
} else {
console.log(`未找到对应的项: ${id}`)
}
},
handleContentClick(id) {
const item = this.arr1.find(item => item.id === id) // 通过子组件传递过来的id,查找对应的子组件
if (item) {
console.log(`${item.content},${item.remark}`)
} else {
console.log(`未找到对应的项: ${id}`)
}
}
}
}
</script>
<template>
<div>
<MyComponent v-for="item in arr1" @title-click="handleTitleClick" @content-click="handleContentClick" :key="item.id" :id="item.id" :title="item.title" :content="item.content" />
</div>
</template>
点击标题,效果如下:

可见,在开发模式( npm run dev )下,如果校验失败,仍然会emit到父组件,但是会同时收到一个警告信息。
参考
https://cn.vuejs.org/guide/essentials/component-basics#listening-to-eventshttps://cn.vuejs.org/guide/components/events.htmlhttps://cn.vuejs.org/api/component-instance.html#emithttps://cn.vuejs.org/api/options-state.html#emits