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 辅助生成,并由作者整理审核。

相关推荐
AlienZHOU2 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc5 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师6 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen7 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒7 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow7 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8187 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800787 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明8 小时前
第 6 章 组件进阶
前端·vue.js
江华森8 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端