Code前端首页关于Code前端联系我们

Vue3里defineModel和emit咋选?用法、区别、实战全解析

terry 15小时前 阅读数 298 #Vue
文章标签 defineModel emit

defineModel和emit在Vue3里是干啥的?

很多刚学Vue3的同学,看到defineModelemit容易犯懵,得先把它们的“角色”搞清楚。

emit是Vue里“子传父”的经典工具,比如子组件里点个按钮,要通知父组件更新数据,就靠$emit触发事件、把数据传给父组件,像子组件写 emit('handleClick', 123),父组件用 <Child @handleClick="parentFn" /> 接收,这就是典型的“事件通信”逻辑。

defineModel呢?它是Vue3.4+推出的双向绑定语法糖,专门简化“父子组件双向同步数据”的场景,以前实现v-model双向绑定,得手动写props接收值、再用emit触发update事件;现在用defineModel,一行代码就能搞定双向绑定,内部自动帮你处理propsemit的逻辑。

用法上,defineModel和emit到底咋写?

先看emit实现双向绑定的“老写法”(现在也能用,但步骤多):
子组件要让父组件的v-model生效,得做两步:

  1. props接收父组件传的modelValue(如果是v-model:xxx,就接收xxx);
  2. 子组件数据变化时,用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是基于emitprops的语法糖

Vue底层处理defineModel时,会自动帮你做这几件事:

  1. 生成一个props,名字就是defineModel的参数(比如defineModel('foo')props就有foo);
  2. 生成对应的update事件(也就是update:foo);
  3. 当你修改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数据,必须是响应式的(比如用refreactive包裹),如果父组件传的是普通变量(比如let a = 1),子组件修改defineModelvalue时,父组件数据不会更新(因为不是响应式)。避坑方法:父组件一定要用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,更灵活,支持多参数、复杂逻辑;
  • 底层原理要搞透?记住defineModelemit+props的语法糖,核心是减少重复代码;
  • 踩坑点要避开?别和props重名、父组件数据要响应式、TS要写类型。

其实两者不是“非此即彼”的关系——Vue3给了更多选择,理解场景后“按需使用”,代码会更简洁高效~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门