Vue 3 深度解析:defineModel() 与 defineProps() 的区别与底层机制

Vue 3.3 引入了一个重要的新特性 ------ defineModel()

它让组件间的双向数据绑定变得更自然、更简洁。

但同时,也引发了很多开发者的疑问:

"modelprops 到底有什么区别?"

"它们在底层是怎么实现的?"

本文将从编译器与运行时两个层面,深入剖析两者的差异与协作机制。


一、概念层面:Vue 中的数据传递两种模式

在 Vue 组件系统中,数据从父组件传递到子组件有两种基本方式:

名称 功能说明
props 父组件向子组件传递只读数据(单向数据流)
model 父子组件间建立双向绑定(数据可从子更新到父)

1️⃣ props:单向输入

xml 复制代码
<!-- 父组件 -->
<Child :title="pageTitle" />
xml 复制代码
<!-- 子组件 -->
<script setup>
const props = defineProps<{ title: string }>()
</script>

<template>
  <h1>{{ props.title }}</h1>
</template>
  • 父组件通过 :title="pageTitle" 传值;

  • 子组件通过 props.title 接收;

  • 若子组件尝试修改 props.title,Vue 会发出警告:

    markdown 复制代码
    [Vue warn]: Attempting to mutate prop "title".

2️⃣ model:双向绑定

xml 复制代码
<!-- 父组件 -->
<Child v-model:title="pageTitle" />
xml 复制代码
<!-- 子组件 -->
<script setup>
const title = defineModel<string>('title')
</script>

<template>
  <input v-model="title" />
</template>
  • 父组件使用 v-model:title 建立双向绑定;
  • 子组件的 title 是一个可读写的 ref;
  • 子修改后会自动触发 emit('update:title'),同步父组件数据。

二、原理层面:编译器的行为差异

Vue 的 <script setup> 编译器在解析这两个宏函数时,生成的底层代码完全不同。


1️⃣ defineProps() 的编译展开

c 复制代码
const props = defineProps<{ title: string }>()

➡️ 编译结果:

javascript 复制代码
export default {
  props: { title: String },
  setup(__props) {
    const props = __props
    return { props }
  }
}

defineProps() 仅生成 props 声明。

子组件只能读取,无法直接修改。


2️⃣ defineModel() 的编译展开

ini 复制代码
const count = defineModel<number>('count')

➡️ 编译结果:

javascript 复制代码
export default {
  props: { count: Number },
  emits: ['update:count'],
  setup(__props, { emit }) {
    const count = computed({
      get: () => __props.count,
      set: v => emit('update:count', v)
    })
    return { count }
  }
}

defineModel() 在编译阶段自动生成:

  • props 定义;
  • 对应的 emits
  • 一个可双向同步的 computed ref

这就是它实现双向数据流的核心机制。


三、对比层面:结构化分析

特性 defineProps defineModel
作用方向 单向:父 → 子 双向:父 ⇄ 子
底层生成 props props + emits + computed
子组件修改 ❌ 不允许 ✅ 可直接修改
触发事件 自动 emit('update:xxx')
绑定语法 :prop="value" v-model[:name]="value"
典型场景 配置性参数、静态内容 状态同步、表单控件
TypeScript 支持 可定义接口 自动类型推导
默认值定义 { default: ... } { default: ... }
可组合性 只读引用 可与 watch/computed 配合双向操作

四、实践层面:父子组件双向同步案例

✅ 子组件:ChildCounter.vue

xml 复制代码
<script setup lang="ts">
const count = defineModel<number>('count')

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    Child Count: {{ count }}
  </button>
</template>

✅ 父组件:Parent.vue

xml 复制代码
<script setup lang="ts">
import ChildCounter from './ChildCounter.vue'
import { ref } from 'vue'

const parentCount = ref(0)
</script>

<template>
  <div>
    <ChildCounter v-model:count="parentCount" />
    <p>Parent Count: {{ parentCount }}</p>
  </div>
</template>

🔄 运行效果说明:

操作 子组件行为 父组件行为
点击按钮 count.value++ 自动触发 emit('update:count')
父组件修改 parentCount 子组件 count 自动更新 ✅ 数据同步

双向流通的闭环:

objectivec 复制代码
Parent state ⇄ props.count ⇄ computed setter

五、拓展层面:defineModel() 的强大特性

1️⃣ 支持多 model

ini 复制代码
const title = defineModel<string>('title')
const checked = defineModel<boolean>('checked')

父组件:

ini 复制代码
<Child v-model:title="text" v-model:checked="flag" />

编译器自动生成:

vbnet 复制代码
props: { title: String, checked: Boolean },
emits: ['update:title', 'update:checked']

2️⃣ 默认值与类型推导

arduino 复制代码
const model = defineModel<number>({ default: 10 })
  • 自动给 props 添加 default
  • TypeScript 类型自动继承;
  • 父组件不传值时,model.value 默认是 10

3️⃣ 只读与必传模式

c 复制代码
const readonlyModel = defineModel<string>({ required: true })

编译后生成:

yaml 复制代码
props: { modelValue: { type: String, required: true } }

六、潜在问题与注意事项

问题场景 描述 解决方案
同名 propsmodel 冲突(编译报错) 避免重复命名
父组件未传入绑定 值为 undefined 使用 { default: xxx }
子组件内部直接修改 props Vue 警告 使用 model
动态 model 名字 必须是常量字符串 不支持表达式
旧版 v-model modelValue 仍兼容 建议迁移新语法

七、底层实现思维导图

arduino 复制代码
        defineModel()
            ↓
      ┌──────────────┐
      │ 编译阶段展开 │
      └──────────────┘
            ↓
 props + emits + computed
            ↓
 双向数据同步:
  props.get  → 父→子
  emit.set   → 子→父

八、模型与属性的协同用法

有时组件既需要配置型参数(props),又需要同步状态(model):

xml 复制代码
<script setup>
const label = defineProps<{ label: string }>()
const checked = defineModel<boolean>('checked')
</script>

<template>
  <label>
    {{ label }}
    <input type="checkbox" v-model="checked" />
  </label>
</template>

父组件:

ini 复制代码
<MySwitch label="Enable Feature" v-model:checked="isEnabled" />

✅ 数据结构清晰:

  • label:静态描述;
  • checked:可响应双向更新。

九、总结:两者的根本区别

对比项 defineProps defineModel
本质 只读输入接口 可写的双向绑定接口
方向 父 ➜ 子 父 ⇄ 子
修改数据 不允许(Vue 警告) 允许(通过 setter)
底层生成 props props + emits + computed
使用场景 组件配置、样式、静态数据 表单、状态同步
使用体验 传统单向流 更自然的双向流
TS 推导 需手动声明类型 自动推导类型

十、思考与展望

defineModel() 并不是简单的语法糖,而是:

  • Vue 响应式设计的自然演化
  • props 与 emits 的统一抽象
  • 为双向状态同步建立更明确的编译约束

未来 Vue 的编译器可能继续扩展:

  • 支持多层嵌套的 v-model
  • 类型推导更智能;
  • 模型声明与表单组件的融合(如 FormKit 风格)。

总结一句话:

在 Vue 3.3+ 中,defineProps() 定义单向输入接口,

defineModel() 定义双向绑定接口。

它们的底层逻辑是互补的 ------

前者提供"父到子"的只读流,后者建立"父 ⇄ 子"的双向响应流。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
excel2 小时前
深入解析 processDefineExpose:Vue SFC 编译阶段的辅助函数
前端
dcloud_jibinbin2 小时前
【uniapp】小程序体积优化,分包异步化
前端·vue.js·webpack·性能优化·微信小程序·uni-app
桜吹雪2 小时前
自定义instanceof运算符行为API: Symbol.hasInstance
前端
qq_427506082 小时前
基于Vue 3和Element Plus实现简单的钩子函数管理各类弹窗操作
前端·javascript·vue.js
excel2 小时前
深入解析:ScriptCompileContext —— Vue SFC 脚本编译上下文的核心机制
前端
粥里有勺糖2 小时前
视野修炼-技术周刊第126期 | TypeScript #1
前端·node.js·github
冰暮流星2 小时前
css3新增过渡
前端·css·css3
天黑请闭眼3 小时前
视频文件上传至服务器后浏览器无法在线播放
前端
一位搞嵌入式的 genius3 小时前
前端实战开发(四):从迭代器到异步编程:ES6 Generator 全面解析 + 实战问题排查
开发语言·前端·es6·前端实战