Vue3里defineModel和emit咋选?用法、区别、实战全解析
defineModel和emit在Vue3里是干啥的?
很多刚学Vue3的同学,看到defineModel和emit容易犯懵,得先把它们的“角色”搞清楚。
emit是Vue里“子传父”的经典工具,比如子组件里点个按钮,要通知父组件更新数据,就靠$emit触发事件、把数据传给父组件,像子组件写 emit('handleClick', 123),父组件用 <Child @handleClick="parentFn" /> 接收,这就是典型的“事件通信”逻辑。
defineModel呢?它是Vue3.4+推出的双向绑定语法糖,专门简化“父子组件双向同步数据”的场景,以前实现v-model双向绑定,得手动写props接收值、再用emit触发update事件;现在用defineModel,一行代码就能搞定双向绑定,内部自动帮你处理props和emit的逻辑。
用法上,defineModel和emit到底咋写?
先看emit实现双向绑定的“老写法”(现在也能用,但步骤多):
子组件要让父组件的v-model生效,得做两步:
- 用
props接收父组件传的modelValue(如果是v-model:xxx,就接收xxx); - 子组件数据变化时,用
emit('update:modelValue', 新值)通知父组件更新。
举个例子,子组件叫MyInput:
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
父组件用的时候:
<MyInput v-model="parentValue" />
现在用defineModel改写子组件,直接“一行顶两行”:
<script setup>
const modelValue = defineModel() // 对应v-model的默认绑定;也能写defineModel('xxx')对应v-model:xxx
function onInput(e) {
modelValue.value = e.target.value // 直接赋值,内部自动emit更新父组件
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
是不是简洁太多?defineModel返回的是可写的ref,修改它的value时,会自动触发update:xxx事件通知父组件——相当于把“写props+写emit”的重复代码全封装了。
从原理看,defineModel和emit啥关系?
一句话总结:defineModel是基于emit和props的语法糖。
Vue底层处理defineModel时,会自动帮你做这几件事:
- 生成一个
props,名字就是defineModel的参数(比如defineModel('foo'),props就有foo); - 生成对应的
update事件(也就是update:foo); - 当你修改
defineModel返回的ref时,内部自动调用emit('update:foo', 新值)。
所以本质上,defineModel没新增逻辑,只是把“写props+写emit”的重复步骤打包了,让双向绑定写起来像操作“本地变量”一样丝滑。
啥场景用defineModel?啥场景必须用emit?
先看defineModel的“舒适区”:需要“父子数据双向同步”的场景,比如自定义表单组件(输入框、下拉框、开关)、UI库的基础组件(像Element Plus的ElInput),这些组件需要和父组件的v-model双向绑定,用defineModel能少写大量重复代码。
再看emit的“主战场”:不需要双向绑定,只需要“子通知父、让父做动作”的场景。
- 子组件点击按钮,让父组件刷新列表(父组件执行
fetchData函数); - 子组件传多个参数给父组件(比如
emit('submit', {name, age, sex})); - 非
v-model的双向场景(比如父组件传两个值,子组件改其中一个、另一个不变,这时候用emit更灵活)。
举个emit的例子,子组件是个“删除按钮”:
<script setup>
const emit = defineEmits(['deleteItem'])
function handleDelete() {
emit('deleteItem', 1001) // 传要删除的ID
}
</script>
<template>
<button @click="handleDelete">删除第1001条</button>
</template>
父组件接收事件、执行删除逻辑:
<DeleteButton @deleteItem="deleteItemFn" />
这种场景用defineModel就不合适——因为不需要双向绑定数据,只是触发父组件的动作。
用defineModel容易踩哪些坑?咋避开?
坑1:和props命名冲突
如果子组件里用了defineModel('foo'),又手动写defineProps(['foo']),就会冲突报错,因为defineModel已经自动生成了foo这个props,重复定义肯定冲突。避坑方法:defineModel定义的变量,别和手动写的props重名。
坑2:响应式丢失/不生效
父组件传给子组件的v-model数据,必须是响应式的(比如用ref或reactive包裹),如果父组件传的是普通变量(比如let a = 1),子组件修改defineModel的value时,父组件数据不会更新(因为不是响应式)。避坑方法:父组件一定要用ref来存v-model绑定的数据。
坑3:TypeScript类型定义
用TS时,defineModel可以传类型参数,
const modelValue = defineModel<number>() // 限定值为数字类型
如果是自定义v-model(比如v-model:age),要这样写:
const age = defineModel<number>('age')
这样父组件传值时类型不对会直接报错,提前拦截错误。
Vue3组合式API里,emit有啥新变化?
在Vue3的setup语法糖里,emit得用defineEmits来声明,和选项式API的this.$emit不一样了。
声明事件:
<script setup>
// 方式1:简单声明事件名
const emit = defineEmits(['handleClick', 'update:foo'])
// 方式2:TS类型验证(更严谨,传参类型不对直接报错)
const emit = defineEmits<{
(event: 'handleClick', id: number): void
(event: 'update:foo', value: string): void
}>()
</script>
触发事件:
emit('handleClick', 123) // 传参数
和Vue2比,变化有这些:
- 必须用
defineEmits声明事件,不然TS会报错(更规范,防止拼错事件名); - 支持TS的“事件类型验证”,传参类型不对直接编译报错;
- 配合
v-model时,要手动写update:xxx事件(而defineModel帮你省了这步)。
实战对比!用defineModel做双向组件 vs 用emit做事件通信
案例1:用defineModel做“自定义输入框”(双向绑定)
需求:子组件是个带前缀的输入框,和父组件的searchValue双向同步。
子组件PrefixInput.vue:
<script setup>
const modelValue = defineModel() // 对应父组件v-model绑定的searchValue
const prefix = defineProps(['prefix']) // 前缀文字,单向传值
</script>
<template>
<div class="prefix-input">
<span>{{ prefix }}</span>
<input
:value="modelValue"
@input="e => modelValue.value = e.target.value"
/>
</div>
</template>
<style scoped>
.prefix-input {
display: flex;
align-items: center;
}
</style>
父组件用的时候:
<script setup>
import { ref } from 'vue'
import PrefixInput from './PrefixInput.vue'
const searchValue = ref('') // 必须用ref保证响应式
</script>
<template>
<PrefixInput v-model="searchValue" prefix="搜索:" />
<p>父组件的值:{{ searchValue }}</p>
</template>
输入子组件的输入框,父组件的searchValue会自动同步;反之,父组件改searchValue,子组件也会更新。
案例2:用emit做“删除确认”(事件通信)
需求:子组件是删除按钮,点击后弹出确认框,确认后通知父组件删除数据。
子组件DeleteButton.vue:
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['confirmDelete'])
const showConfirm = ref(false)
function handleClick() {
showConfirm.value = true
}
function onConfirm() {
emit('confirmDelete') // 触发父组件的删除逻辑
showConfirm.value = false
}
</script>
<template>
<button @click="handleClick">删除</button>
<div v-if="showConfirm" class="confirm-modal">
<p>确定要删除吗?</p>
<button @click="onConfirm">确认</button>
<button @click="showConfirm = false">取消</button>
</div>
</template>
<style scoped>
.confirm-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 1px solid #eee;
}
</style>
父组件用的时候:
<script setup>
import { ref } from 'vue'
import DeleteButton from './DeleteButton.vue'
const list = ref(['数据1', '数据2', '数据3'])
function deleteItem() {
list.value.pop() // 删除最后一条
}
</script>
<template>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<DeleteButton @confirmDelete="deleteItem" />
</template>
点击子组件的删除按钮,弹出确认框;点“确认”后,父组件执行deleteItem,列表最后一条被删除,这种场景用emit更灵活——因为不需要双向绑定数据,只是触发父组件的方法。
最后总结:到底咋选?
- 想“偷懒”写双向绑定?选
defineModel,适合v-model场景,代码少一半; - 只需要“子通知父做事、传数据”?选
emit,更灵活,支持多参数、复杂逻辑; - 底层原理要搞透?记住
defineModel是emit+props的语法糖,核心是减少重复代码; - 踩坑点要避开?别和
props重名、父组件数据要响应式、TS要写类型。
其实两者不是“非此即彼”的关系——Vue3给了更多选择,理解场景后“按需使用”,代码会更简洁高效~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


