Vue emit 子组件向父组件传值示例
emit 解决的是什么问题
Vue 的数据流是单向的:父组件通过 props 把数据传给子组件,子组件不能直接改写收到的 props。一旦子组件在内部对 props 赋值,开发环境会立刻收到 Avoid mutating a prop directly 的警告,因为父组件下一次重新渲染仍会用自己的数据覆盖掉子组件的改动,两边状态就对不上了。
所以子组件想「改父组件的值」,正确做法是拆成两步:子组件用 $emit 抛出一个自定义事件并带上新值,父组件监听这个事件、在自己的作用域里完成赋值。数据的所有权始终留在父组件手里,子组件只是发出请求。v-model 和 Vue 2 的 .sync 修饰符,底层做的就是这件事。
下面这组示例把三种常见写法排在一起对比:模板里内联赋值、父组件用方法接收、以及事件名用 update:xxx 约定名与用自定义名的区别。
vue2 中 emit 示例
父组件里同时挂了三个子组件,分别对应三种接收方式。update:属性名 这种事件名是一个约定,它能配合 .sync 修饰符使用::title.sync="title" 等价于手写 :title="title" @update:title="title = $event"。注意示例中事件名是 update:title,而 props 名叫 emitStringOne,两者并不对应,所以这里只能手写监听,不能直接换成 .sync。
父组件
<template>
<div>
<EmitChildOne
:emitStringOne="emitStringOne"
@update:title="val => (emitStringOne = val)"
></EmitChildOne>
<EmitChildTwo
:emitStringTwo="emitStringTwo"
@update:title="updateEmitTwo"
></EmitChildTwo>
<EmitChildThree
:emitStringThree="emitStringThree"
@updateTitle="updateEmitThree"
></EmitChildThree>
</div>
</template>
<script>
import EmitChildOne from './emitChildOne.vue';
import EmitChildTwo from './emitChildTwo.vue';
import EmitChildThree from './emitChildThree.vue';
export default {
components: {
EmitChildOne,
EmitChildTwo,
EmitChildThree,
},
data() {
return {
emitStringOne: 'emitStringOne',
emitStringTwo: 'emitStringTwo',
emitStringThree: 'emitStringThree',
};
},
methods: {
updateEmitTwo(val) {
this.emitStringTwo = val;
},
updateEmitThree(val) {
this.emitStringThree = val;
},
},
};
</script>
第一个子组件里给出了两种触发方式:一种是在 methods 里定义 test 方法再调用 this.$emit,另一种是直接在模板的事件处理里写 $emit(...)。模板里能直接用 $emit 是因为它挂在组件实例上,模板表达式的作用域就是当前实例。逻辑简单时用内联写法更省事,需要做校验或者拼装数据时再抽成方法。
子组件 emitChildOne.vue
<template>
<div>
<div>值:{{ emitStringOne }}</div>
<button @click="test">测试</button>
<br />
<button @click="$emit('update:title', '测试直接 emit')">
测试直接 emit
</button>
</div>
</template>
<script>
export default {
props: {
emitStringOne: String,
},
methods: {
test() {
this.$emit(
'update:title',
'emitChildOne:emitStringOne:我在子组件中的值',
);
},
},
};
</script>
第二个子组件抛出的事件名和第一个完全一样,都是 update:title。这说明事件名的作用域只在当前这一对父子之间,不同子组件用同名事件互不干扰,父组件在各自的标签上分别监听即可。父组件这次没有用内联赋值,而是把处理逻辑放进 updateEmitTwo 方法,值多、需要额外处理时这种写法更清晰。
子组件 emitChildTwo.vue
<template>
<div>
<div>值:{{ emitStringTwo }}</div>
<button @click="test">测试</button>
</div>
</template>
<script>
export default {
props: {
emitStringTwo: String,
},
methods: {
test() {
this.$emit(
'update:title',
'emitStringTwo:emitStringTwo:我在子组件中的值',
);
},
},
};
</script>
第三个子组件换成了自定义事件名 updateTitle,父组件对应写成 @updateTitle。这里有个容易踩的坑:HTML 标签的属性名不区分大小写,如果模板写在 .html 文件里而不是单文件组件里,@updateTitle 会被浏览器解析成全小写的 @updatetitle,事件就监听不到了。稳妥的做法是事件名统一用 kebab-case,也就是子组件抛 update-title、父组件监听 @update-title。
子组件 emitChildThree.vue
<template>
<div>
<div>值:{{ emitStringThree }}</div>
<button @click="test">测试</button>
</div>
</template>
<script>
export default {
props: {
emitStringThree: String,
},
methods: {
test() {
this.$emit(
'updateTitle',
'emitStringThree:emitStringThree:我在子组件中的值',
);
},
},
};
</script>

还没有留言,来发表第一条吧~