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

参数

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

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

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

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

常见的字符串格式如下:

  • kebab-case ,比如: how-are-you
  • camelCase ,比如: howAreYou
  • PascalCase ,比如: 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推荐使用显式声明。原因有二:

  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
相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄5 天前
JavaScript 隐式全局变量解析
javascript
汉堡大王95275 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大5 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师5 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端