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

Vue3里v - model和defineModel咋区分?用法、原理、场景全解析

terry 4小时前 阅读数 89 #Vue
文章标签 model;defineModel

不少刚接触Vue3的同学,一看到v - model和defineModel就犯迷糊:这俩啥关系?在啥场景用?该咋选?今天咱用问答形式,把这些疑惑一次性讲明白。

问题1:Vue3的v - model基础用法是啥?和Vue2有啥不一样?

双向绑定的核心逻辑是父组件传值给子组件,子组件变化后通知父组件更新,v - model就是为了让双向绑定写起来更简便的语法糖。

在Vue2里,子组件用v - model得这么写:

<!-- 子组件(Vue2) -->
<template><input :value="value" @input="$emit('input', $event.target.value)" /></template>
<script>
export default { props: ['value'], emits: ['input'] }
</script>

父组件用<Child v - model="parentVal"/>,本质是value="parentVal"结合@input="parentVal = $event"

到了Vue3,默认的props名和事件名变了:props默认叫modelValue,事件默认叫update:modelValue,所以Vue3里不用defineModel的传统写法得这样:

<!-- 子组件(Vue3传统写法) -->
<template><input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /></template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

父组件用法依旧是<Child v - model="parentVal"/>,只是底层的props和事件名有变化,这样改是为了让多参数v - model(比如v - model:foo)的语法更统一,后面讲多绑定场景时会更直观。

问题2:defineModel是干啥的?为啥说它简化了v - model开发?

Vue3.4之后新增的defineModel编译时宏(可以理解成“语法层面的快捷方式”),专门解决子组件用v - model时“样板代码太多”的问题。

以前子组件要支持v - model,得手动做这些事:

  • defineProps声明接收modelValue
  • defineEmits声明触发update:modelValue
  • 模板里还要把输入事件和$emit绑定。

现在有了defineModel,一行代码就能搞定所有逻辑:

<!-- 子组件(用defineModel) -->
<template><input v - model="model" /></template>
<script setup>
import { defineModel } from 'vue'
const model = defineModel() // 这一行就等同于上面两行defineProps + defineEmits
</script>

你可以把model当作本地响应式变量来用,修改model.value时,会自动触发父组件的update:modelValue事件,不用手动写$emit,相当于Vue帮你把“声明props + 声明事件 + 触发事件”全自动化了,代码量直接减半!

问题3:defineModel实际项目咋用?能给个完整例子不?

咱以“父组件和子组件双向绑定用户名”为例,分别看父、子组件的代码和效果。

父组件(Parent.vue):

<template>
  <div>父组件显示:{{ username }}</div>
  <!-- 用v - model绑定子组件 -->
  <MyInput v - model="username" />
  <button @click="reset">重置用户名</button>
</template>
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
const username = ref('初始名')
function reset() {
  username.value = '初始名' // 父组件改值,子组件会同步
}
</script>

子组件(MyInput.vue):

<template>
  <!-- 直接用model双向绑定输入框 -->
  <input type="text" v - model="model" placeholder="输入新用户名" />
  <button @click="forceChange">强制改成“测试”</button>
</template>
<script setup>
import { defineModel } from 'vue'
const model = defineModel() // 自动关联父组件的v - model
function forceChange() {
  model.value = '测试' // 子组件改值,父组件会同步
}
</script>

运行后能看到:

  • 在输入框打字,父组件的username会跟着变;
  • 点击父组件“重置”,子组件输入框会变回“初始名”;
  • 点击“强制改成测试”,父组件username会变成“测试”。

整个过程不用手动写propsemit,全靠defineModel自动处理双向绑定,是不是很省心?

问题4:v - model和defineModel的原理是啥?编译后发生了啥?

得从“语法糖”和“编译时处理”这两个角度来理解:

v - model的原理:

父组件写<Child v - model="x"/>,Vue编译后会变成:

<Child :modelValue="x" @update:modelValue="x = $event" />

所以v - model本质是“绑定props + 监听自定义事件”的语法糖,让双向绑定写起来更简洁。

defineModel的原理:

defineModel编译时宏(只有在<script setup>里能用,因为编译阶段要做特殊处理),当你写const model = defineModel()时,Vue编译阶段会自动帮你:

  1. 生成defineProps(['modelValue'])(声明接收父组件的modelValue);
  2. 生成defineEmits(['update:modelValue'])(声明触发更新事件);
  3. 返回一个代理变量model,修改model.value时,自动调用$emit('update:modelValue', 新值)

所以你看到的“直接改model.value”,实际是Vue在编译后帮你做了$emit,底层还是基于“props向下传,事件向上抛”的单向数据流,没有破坏Vue的核心设计。

问题5:多个v - model绑定咋处理?defineModel支持多参数吗?

实际开发中,经常需要一个子组件双向绑定多个父组件变量(比如同时绑定“用户名”和“密码”),Vue3的v - model支持加参数,defineModel也能对应处理。

举个例子:父组件同时绑定usernamepassword,子组件分别处理。

父组件(MultiBind.vue):

<template>
  <div>
    用户名:{{ username }} <br />
    密码:{{ password }} <br />
  </div>
  <MyForm 
    v - model="username" 
    v - model:password="password" 
  />
</template>
<script setup>
import { ref } from 'vue'
import MyForm from './MyForm.vue'
const username = ref('')
const password = ref('')
</script>

子组件(MyForm.vue):

<template>
  <input placeholder="用户名" v - model="nameModel" />
  <input type="password" placeholder="密码" v - model="pwdModel" />
  <button @click="randomFill">随机填充</button>
</template>
<script setup>
import { defineModel } from 'vue'
// 处理默认v - model(对应username)
const nameModel = defineModel() 
// 处理v - model:password(参数是password)
const pwdModel = defineModel('password') 
function randomFill() {
  nameModel.value = '随机名' + Math.random().toString().slice(2, 5)
  pwdModel.value = 'pwd' + Math.random().toString().slice(2, 5)
}
</script>

这里的关键是:

  • 父组件用v - model:参数名绑定多个变量;
  • 子组件用defineModel('参数名')对应接收,每个defineModel实例管理一个双向绑定;
  • 修改nameModel.valuepwdModel.value时,各自触发对应的update:参数名事件,父组件对应变量更新。

要是用传统写法(手动写props和emit),得声明props: ['modelValue', 'password']emits: ['update:modelValue', 'update:password'],再手动$emit,代码量会翻倍,用defineModel就简洁多了!

问题6:从Vue2升级到Vue3,v - model和defineModel要注意哪些点?

升级时主要解决“语法兼容”和“代码迁移”问题:

Vue2 → Vue3的v - model语法变化

Vue2子组件v - model依赖props: value + emit: input,Vue3默认改成props: modelValue + emit: update:modelValue,所以升级老组件时,得把:

  • props: { value: ... } 改成 props: { modelValue: ... }
  • $emit('input', 新值) 改成 $emit('update:modelValue', 新值)

用defineModel替代传统写法

如果项目用Vue3.4+,可以直接用defineModel替换手动声明的propsemit,减少代码,比如把上面升级后的传统写法,改成:

<template><input v - model="model" /></template>
<script setup>
const model = defineModel() // 自动处理modelValue和update:modelValue
</script>

多参数绑定的兼容(Vue2的.sync修饰符)

Vue2里用.sync实现多参数双向绑定(比如<Child :foo.sync="x"/>),Vue3里统一成v - model:foo语法,defineModel也支持defineModel('foo'),所以升级时把.sync改成v - model:foo,子组件用defineModel('foo')就行。

问题7:defineModel只能用在