我们知道,Props 实现的父子组件数据传递遵循单向数据流原则,子组件不可以擅自更改父组件传过来的值。
那么,如果子组件确实需要更新这些数据该怎么办呢?
此时就需要使用 emit 自定义事件。子组件通过 emit 向父组件发送通知,由父组件负责修改数据,再将最新的数据通过 Props 传递给子组件。整个过程遵循 “谁拥有数据,谁负责修改数据” 的设计原则。
具体流程如下:
• 父组件:「有事就告诉我。」(监听事件) • 子组件:「数据需要更新啦!」( emit触发事件并传递数据)• 父组件:「收到,我来修改数据。」(更新 data)• 子组件:「拿到最新数据了。」(通过 Props 重新接收数据,如果有Props数据会重传给子组件)
emit简单例子
我们通过一个最简单的示例来演示子组件如何通过 emit 事件通知父组件更新数据。这里仅展示 emit 的基本用法,实际开发中通常会结合 props 和 emit 一起使用,以实现更完整的父子组件通信。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>简单 Emit 示例</title>
<script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 1. 父组件
const vm = Vue.createApp({
data() {
return {
message: '等待点击...'// 用来接收子组件传来的字
}
},
methods: {
// 这里的 text 就是子组件传上来的数据
reply(text) {
this.message = text;
}
},
// @say="reply" 意思是:当子组件触发 say 事件时,执行父组件的 reply 方法
template: `
<div>
<h2>父组件收到:{{ message }}</h2>
<hr />
<my-button @say="reply"></my-button>
</div>
`
});
// 2. 子组件
vm.component('my-button', {
// 声明组件触发的自定义事件(Vue3 推荐的做法,利于代码可读性)
emits: ['say'],
methods: {
send() {
// 触发 say 事件,并顺便捎带一句话 "来自子组件的问候!"
this.$emit('say', '来自子组件的问候!');
}
},
template: '<button @click="send">点击通知父组件</button>'
});
vm.mount('#root');
</script>
</html>如上面的例子所示,我们做了如下三个步骤:
1. 父组件通过 v-on(或@)监听子组件触发的自定义事件,并在事件触发时执行对应的方法。
<my-button @say="reply"></my-button>
2. 子组件在需要的时候使用 this.$emit通知父组件进行更新。
this.$emit('say', '来自子组件的问候!');3. 父组件收到通知后,会执行对应的事件处理方法
methods: {
// 这里的 text 就是子组件传上来的数据
reply(text) {
this.message = text;
}
},事件校验
和 Props 校验类似,我们也可以对 emit 事件进行校验,下面来看一个例子。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue3 Emit 校验示例</title>
<script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 1. 父组件
const vm = Vue.createApp({
data() {
return {
message: '等待点击...',
code: 0
}
},
methods: {
// 接收子组件传来的多个参数
reply(text, status) {
this.message = text;
this.code = status;
}
},
template: `
<div>
<h2>父组件收到:{{ message }} (状态码: {{ code }})</h2>
<hr />
<my-button @say="reply"></my-button>
</div>
`
});
// 2. 子组件
vm.component('my-button', {
// 【核心改变】:将 emits 改为对象形式,并为 say 事件编写校验函数
emits: {
// say 对应一个函数,函数的参数就是 this.$emit 传出来的参数
say: (text, status) => {
// 校验逻辑:必须传 text,且 status 必须是 200 或者是 404
const isValid = text && [200, 404].includes(status);
if (!isValid) {
console.warn(`❌ [Emit 校验失败]: 传递的参数非法!当前收到 text: ${text}, status: ${status}`);
}
// 必须返回一个布尔值:true 代表校验通过,false 代表校验失败(控制台会报警告)
return isValid;
}
},
methods: {
sendSuccess() {
// 触发 say 事件,传过去的数据符合校验规则 (status 是 200)
this.$emit('say', '数据加载成功!', 200);
},
sendError() {
// 触发 say 事件,传过去的数据不符合校验规则 (status 是 500,不在 [200, 404] 里面)
this.$emit('say', '服务器崩溃啦!', 500);
}
},
template: `
<div>
<button @click="sendSuccess">发送正确数据 (200)</button>
<button @click="sendError" style="margin-left: 10px;">发送错误数据 (500)</button>
</div>
`
});
vm.mount('#root');
</script>
</html>在这个例子中,say 被定义为一个函数,当校验不通过时返回 false,校验通过时返回 true。
注意:事件校验和 Props 校验一样,并不是强制性的,只会在浏览器控制台给出警告,开发者应及时修复这些警告。
更优雅的写法
看下面这个例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-model 改写示例</title>
<script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 1. 父组件
const vm = Vue.createApp({
data() {
return {
message: '等待点击...'// 这个数据将与子组件双向绑定
}
},
// 使用 v-model:model-value(简写为 v-model)直接绑定 message
// 不需要再手动写 @say="reply" 和 methods 了
template: `
<div>
<h2>父组件收到:{{ message }}</h2>
<hr />
<my-button v-model="message"></my-button>
</div>
`
});
// 2. 子组件
vm.component('my-button', {
// Vue3 中默认的 v-model 接收的属性名是 modelValue
props: ['modelValue'],
// 对应的,声明的触发事件必须是 'update:modelValue'
emits: ['update:modelValue'],
methods: {
send() {
// 触发 update:modelValue 事件,直接修改父组件绑定的变量
this.$emit('update:modelValue', '来自子组件的问候!');
}
},
template: '<button @click="send">点击通知父组件</button>'
});
vm.mount('#root');
</script>
</html>我们没有按照上面“三步走”的方式实现,但发现组件内的 message 仍然会被更新,这说明该例子是有效的,而且写法也更加优雅。
这里我们用到了 v-model。v-model 本质上是一个语法糖,它在底层自动帮我们处理了属性绑定(Props)和事件监听(Emits)。接下来我们看看,相比之前的写法,这个例子发生了哪些变化。
1. 父组件视角: 使用的是 v-model,而不是手动的v-bind绑定。2. 子组件的接收:在 Vue 3 中, v-model会被默认拆解,子组件需要通过props: ['modelValue']来接收传递进来的值。3. 子组件的更新:当点击子组件时,执行 this.$emit('update:modelValue', 新值)。其中update:modelValue是 Vue 约定的特殊事件名。父组件监听到该事件后,会自动将子组件传递的新值更新到myCount变量中。
你可能会觉得 modelValue / update:modelValue 这两个名字比较固定,因为它们是 Vue 3 的默认约定。如果不想使用默认命名,也可以进行自定义重命名。
父组件
<my-button v-model:message="message"></my-button>
子组件
vm.component('my-button', {
// Vue3 中默认的 v-model 接收的属性名是 modelValue
props: ['message'],
// 对应的,声明的触发事件必须是 'update:modelValue'
emits: ['update:message'],
methods: {
send() {
// 触发 update:modelValue 事件,直接修改父组件绑定的变量
this.$emit('update:message', '来自子组件的问候!');
}
},
template: '<button @click="send">点击通知父组件</button>'
});