Vue组件通信之emit

文章目录

总结

  • 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-clickcontent-click 两个事件,这不是HTML原生事件,而是从子组件emit过来的自定义事件。

参数

$emit() 方法可以有多个参数:

javascript 复制代码
$emit: (event: string, ...args: any[])

第一个参数是自定义的方法名,是字符串类型,比如本例中的 title-click

这里涉及到了方法名的转换问题。

常见的字符串格式如下:

  • kebab-case ,比如: how-are-you
  • camelCase ,比如: howAreYou
  • PascalCase ,比如: HowAreYou

"子组件在 $emit() 方法里指定的事件名"和"父组件里实际的事件名"在匹配时,遵循如下规则:

  • 大小写敏感,比如 titleClicktitleclick 不匹配
  • 首字母大小写不敏感,比如 titleClickTitleClick 匹配
  • 大写字母和"连字符加小写字母"匹配,比如 titleClicktitle-click 匹配

也就是说,对于同一个字符串来说,其 kebab-casecamelCasePascalCase 格式是相互匹配的。

本例中,第一个参数如果是 titleClickTitleClicktitle-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推荐使用显式声明。原因有二:

  1. 一目了然,知道子组件会emit哪些自定义事件
  2. 更重要的原因是,避免原生的事件在父组件中被触发两次

前面提到,父组件的原生事件会透传到子组件。那么问题来了,如果子组件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 是我自定义的事件",只作为组件事件处理,不会把父组件的原生事件透传到子组件根元素。

总结:

  1. 最好加上 emits 显式声明
  2. 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-events
  • https://cn.vuejs.org/guide/components/events.html
  • https://cn.vuejs.org/api/component-instance.html#emit
  • https://cn.vuejs.org/api/options-state.html#emits
相关推荐
kyriewen1 小时前
线上Bug炸了,用户骂你你却不知道?前端监控教你“远程开天眼”
前端·javascript·监控
网络点点滴1 小时前
创建一个简单的web服务器
运维·服务器·前端
kisloy2 小时前
【反爬虫】极验4 W参数逆向分析
java·javascript·爬虫
XPoet2 小时前
AI 编程工程化:MCP——给你的 AI 员工打通外部能力
前端·后端·ai编程
夏雪之晶莹2 小时前
JSON语法结构
javascript
笨笨狗吞噬者2 小时前
小程序包体积分析利器 -- vite-plugin-component-insight
前端·微信小程序·uni-app
吴声子夜歌2 小时前
Vue3——v-for指令
前端·javascript·vue
你的牧游哥2 小时前
Cursor IDE Rules / Skills / Subagents 前端项目配置全指南
前端·ide
音仔小瓜皮2 小时前
【Vue】什么时候用Ref?什么时候用shallowRef?
前端·javascript·vue.js