从 v-model 到 defineModel:Vue3 正在悄悄改变开发习惯

一、组件化与双向绑定的演变背景

1. 早期前端:手写 DOM 与状态管理痛点

在纯 JavaScript 时代,我们尝尝直接操作 DOM、管理全局变量。随着应用复杂度提升,出现了以下问题:

  • 状态混乱:UI 状态(表单值、控件可见性)散落在各个回调里,难以维护。
  • 模块化隔离不足:不同 UI 模块互相干扰,数据污染严重。
  • 手动同步:每次数据改变,都要手动 querySelector → 更新属性 → 补齐事件监听器,十分臃肿。
2. MVVM 思想与双向绑定萌芽

为了解决手工同步的痛点,AngularJS(1.x)提出了 MVVM 架构,引入双向数据绑定,模板中写一个 ng-model,改变模型(Model)或视图(View)都能自动同步。这大大提升了开发效率,但也带来性能风险——脏检查(digest cycle)在大型应用中很难调优。

Vue 1.0 受到启发,保留双向绑定优势的同时,用基于 Object.defineProperty 的响应式系统来替代脏检查,性能与体验兼得。

二、Vue 2.x 中的 v-model

1. 前言:为什么需要 v-model 呢?

在传统的网页开发中,我们经常要手动将表单输入与 JavaScript 数据做同步:

  1. 监听输入框的 input 或 change 事件;
  2. 在回调中 setState 或直接赋值;
  3. 然后在渲染函数中把数据写回到 DOM。

这种手工绑定不仅代码冗长,而且容易出错,尤其是在多表单、复杂交互场景下,维护成本极高。

Vue 2.x 引入了 MVVM(Model–View–ViewModel)的理念,通过 数据响应式 + 模板指令,让模板到数据、数据到模板的同步都只需一句话:

<input v-model="foo" />

Vue 会在内部搞定:

  • 初始化时把 foo 的值渲染到输入框;
  • 用户输入时自动更新 foo;
  • foo 变化时又反过来更新输入框。

从而极大地简化了表单开发,提升了开发效率。

2. 基本用法

举个 🌰

<div id="app">
  <input v-model="message" placeholder="请输入文本">
  <p>输入的是:{{ message }}</p>
</div>
new Vue({
  el: '#app',
  data: {
    message: ''
  }
});
  • 渲染阶段:Vue 把 data.message 的值赋给 <input> 的 value 属性。
  • 模版绑定:Vue 编译器把 v-model 转为 :value="message" + @input="message = $event.target.value"。
  • 交互阶段:用户输入触发 input 事件,执行赋值,内部触发 数据响应式,重新渲染视图。
2. 在不同表单控件中的应用
  1. <input type="text">、<textarea>:监听 input 事件。
  2. <input type="checkbox">:在 value 与 checked 之间进行双向绑定;
  3. <input type="radio">:将同一组 radio 的 value 与数据做一一映射;
  4. <select>:多选时会将选中值合并为数组。
<!-- 单选 -->
<label><input type="radio" v-model="picked" value="A">A</label>
<label><input type="radio" v-model="picked" value="B">B</label>

<!-- 复选 -->
<label><input type="checkbox" v-model="checks" value="X">X</label>
<label><input type="checkbox" v-model="checks" value="Y">Y</label>

<!-- 下拉 -->
<select v-model="selected">
  <option disabled value="">请选择</option>
  <option>A</option>
  <option>B</option>
</select>
3. v-model 修饰符

Vue 2.x 为 v-model 提供了三个常用修饰符:.lazy、.number.trim,以及组合的 .sync 语法(实为 v-bind.sync 语法糖)。

.lazy
<input v-model.lazy="msg" />
  • 默认:v-model 在 input 事件触发时同步。
  • 懒惰模式:v-model.lazy 在 change 事件触发时才同步(失去焦点或按下回车)。
  • 场景:避免每次按键都触发更新,如大型数据校验、实时搜索时可减少不必要的操作。
.number
<input v-model.number="quantity" type="number">

自动将用户输入的字符串转为数字,相当于:

inputHandler(e) {
  this.msg = Number(e.target.value);
}

注意:Number('') 会转成 0,要防范空字符串带来的歧义。

.trim
<input v-model.trim="username" placeholder="用户名">

自动去除用户输入的首尾空白,相当于:

inputHandler(e) {
  this.msg = e.target.value.trim();
}
.sync(.sync 语法糖)

其实是 v-bind:prop.sync="value" 的简写。 

等价于:

    <Child :foo="bar" @update:foo="val => bar = val" />

    父组件在使用子组件绑定时:

    <my-comp :title.sync="pageTitle" />

    会自动把子组件的 this.$emit('update:title', newVal) 绑定到父组件,达成双向。

    4. 自定义组件中的 v-model

    当把 v-model 用在子组件时,Vue 会默认:

    • prop:value
    • 事件:input

    比如:

    <!-- 父组件 -->
    <my-input v-model="text" />

    等价于:

    <my-input :value="text" @input="val => text = val" />

    在子组件里,需要:

    Vue.component('my-input', {
      props: ['value'],
      template: `<input :value="value" @input="$emit('input', $event.target.value)" />`
    });
    自定义 model 选项

    Vue 2.x 支持在组件中用 model 选项定制 v-model 的 prop 和 event 名称:

    Vue.component('my-cmp', {
      model: {
        prop: 'checked',
        event: 'change'
      },
      props: {
        checked: Boolean
      },
      template: `<input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" />`
    });
    

    父组件写 <my-cmp v-model="flag" /> 时,Vue 会使用 prop=checked、event=change。 

    5. Vue 2.x v-model 的底层原理
    指令系统(Directive)

    v-model 在 Vue 2 中本质是一个 内置指令(model)

    • 编译阶段,模板编译器会识别 v-model 指令,生成对应的 AST 节点;

    • 渲染阶段,为元素注入 model 指令的钩子函数:bind、update、componentUpdated 等。

    // 大致伪代码结构
    model: {
      bind(el, binding, vnode) {
        // 初始化:setInitialValue(el, binding.value)
        // 根据 el.tagName / type 决定事件类型,如 input、change
        el.addEventListener(eventName, handler);
      },
      update(el, binding) {
        // 当数据变化时,更新 el.value = binding.value
      },
      unbind(el, binding) {
        // 清理事件监听器
        el.removeEventListener(eventName, handler);
      }
    }
    
    • bind 钩子:初始化时为元素添加对应的事件监听器,并设置初始值。
    • update 钩子:响应式数据变化后,Vue 会调用指令的 update,保持 DOM 与数据同步。
    AST 转换

    以 <input v-model="foo" /> 为例,编译器处理过程:

    1. 解析模板 → AST:

      {
        tag: 'input',
        attrsList: [{ name: 'v-model', value: 'foo' }],
        // ... 其它节点信息
      }
    2. 生成指令 → 为 AST 添加 model 指令:

      node.directives = [{ name: 'model', rawName: 'v-model', value: 'foo', modifiers: {} }];
      
    3. 代码生成 → 在渲染函数中注入 withDirectives:

      withDirectives(
        createElementVNode('input', { value: foo }),
        [[vModelText, foo]]
      )
      
    运行时:指令 Hook 与响应式

    1、初始化:directive.bind 会调用 el.value = foo,并用 el.addEventListener('input', handler)。

    2、用户输入:handler 执行 foo = el.value,触发 Vue 的响应式赋值。

    3、响应式更新:数据变化经过 Object.defineProperty 的 setter → Dep.notify() → watcher → 重新执行渲染函数 → 再次触发指令的 update,从新同步 el.value。

    数据响应式核心

    Vue 2.x 用 Object.defineProperty 实现响应式:

    1. 遍历 data,对每个属性调用 defineReactive(obj, key, val)。
    2. getter:收集依赖(Dep.target),即当前活跃的 watcher。
    3. setter:更新值并触发 dep.notify(),通知所有 watcher 更新视图。

    v-model 的流程:

    • 数据源:data.foo 被代理到组件实例,拥有 getter/setter。
    • 视图端:directive.bind 通过 el.addEventListener 订阅 input。
    • 数据变更:用户触发 input → setter 执行 → 通知渲染 watcher → 重新渲染 → directive.update 同步 DOM。
    6. 源码分析

    1、指令注册

    // src/platforms/web/runtime/directives/model.js
    export default {
      bind(el, binding, vnode) {
        setModel(el, binding, vnode);
      },
      update(el, binding, vnode) {
        if (binding.value !== binding.oldValue) {
          setModel(el, binding, vnode);
        }
      },
    };
    

    2、模型设置

    function setModel(el, binding, vnode) {
      const { value, modifiers } = binding;
      const tag = el.tagName.toLowerCase();
    
      if (tag === 'input') {
        if (el.type === 'text' || el.type === 'password') {
          el.value = value;                      // 初始值
          el.addEventListener(
            modifiers.lazy ? 'change' : 'input',
            e => {
              let newValue = e.target.value;
              if (modifiers.trim) newValue = newValue.trim();
              if (modifiers.number) newValue = Number(newValue);
              vnode.context[binding.expression] = newValue;
            }
          );
        }
        // 省略 radio / checkbox 逻辑
      } else if (tag === 'select') {
        // select 多选/单选处理...
      }
    }
    

    三、Vue 3 中 v-model 的重构

    1. 为什么要重构 v-model 呢?
    Vue 2 的困境

    在 Vue 2.x 时代,v-model 已经是表单开发的王炸指令,但也存在一些局限:

    • 默认只能单一绑定
      • 组件上只能有一个 v-model,对应默认的 value/input。如果想对不同字段做双向绑定,就必须手动写 :foo + @update:foo 或使用 .sync,很不直观。
    • 修饰符语法零散
      • .lazy、.number、.trim 三个修饰符虽然能满足大部分需求,但并不能自定义,也不能应用到组件层面。
    • TypeScript 支持不友好
      • 在组件内部用 v-model 时,仍必须显式地在 props 中声明 value,在 emits 中声明 input,样板代码冗余。
    Vue 3 的目标

    Vue 3 团队希望在保留 v-model 开发便捷性的同时:

    1. 支持多模型绑定,让同一个组件能暴露多个 v-model,且语法统一。
    2. 修饰符更灵活,支持自定义绑定时机、预处理管道。
    3. 与 Composition API 深度融合,在 <script setup> 中减少样板、提升 TS 体验。
    4. 底层实现更模块化,让指令、编译器能更好地演进和扩展。
    2. Vue 3 中 v-model 的新语法与用法
    默认 v-model:modelValue + update:modelValue

    在 Vue 3 中,单一 v-model 绑定变成:

    <!-- 模板写法 -->
    <ChildComponent v-model="foo" />
    
    <!-- 编译后等价于 -->
    <ChildComponent
      :modelValue="foo"
      @update:modelValue="val => foo = val"
    />
    
    • Prop 名称:modelValue
    • 事件名称:update:modelValue

    这样避免了 Vue 2 的 value/input 语义冲突,同时与「单向数据流」的概念更契合:prop 向下传递,事件向上传递。

    多模型绑定

    Vue 3 支持在同一个组件上使用多次 v-model,并且可自定义绑定的字段名:

    <MyComp
      v-model:title="pageTitle"
      v-model:visible="isDialogVisible"
    />

    等价于:

    <MyComp
      :title="pageTitle"
      @update:title="val => pageTitle = val"
      :visible="isDialogVisible"
      @update:visible="val => isDialogVisible = val"
    />
    

    任意 prop 都可以变成双向绑定。

    格式:v-model:arg="value" → :arg="value" + @update:arg="value = $event"

    v-model 修饰符的变化

    Vue 3 仍保留 .lazy、.number、.trim,但在组件层面也可通过 modelModifiers 传递到子组件上:

    <!-- 在父组件 -->
    <Child v-model.lazy.number.trim="foo" />
    
    <!-- 会将 { lazy: true, number: true, trim: true } 作为 modelModifiers 传给子组件 -->
    <Child
      :modelValue="foo"
      @update:modelValue="foo = $event"
      :modelModifiers="{ lazy: true, number: true, trim: true }"
    />
    

    子组件可在 v-bind="$attrs" 中接收 modelModifiers,或通过 defineProps({ modelModifiers: Object }) 拿到修饰符描述,自定义处理。

    3. 底层编译器如何处理 v-model
    AST 阶段指令转换

    以 <input v-model="msg" /> 为例,Vue 3 编译器在解析模板阶段会:

    1. 生成 AST,节点包含 directive: { name: 'model', arg: null, exp: 'msg', modifiers: {} }
    2. 转换指令:将 v-model 转为两条指令:
      1. v-bind:modelValue="msg"

      2. v-on:update:modelValue="($event) => msg = $event"​​​​​​

    3. 最终生成渲染函数

      createVNode(
        'input',
        {
          modelValue: msg,
          'onUpdate:modelValue': $event => (msg = $event)
        }
      )
      

    真正的编译产物中,已经看不到 v-model,只剩下标准的 bind 与 on。

    运行时指令与组件支持

    对于原生元素,Vue 3 还提供了专门的 vModelXXX 指令模块,如 vModelText、vModelCheckbox、vModelRadio、vModelSelect,它们统一挂在 withDirectives 中:

    withDirectives(
      createVNode('input', { type: 'text', modelValue: msg }),
      [[vModelText, msg]]
    )
    
    • vModelText 内部:在 beforeMount 钩子设置 el.value = msg,mounted 钩子注册 input 事件。
    • 组件:对于自定义组件,v-model 转为 modelValue + update:modelValue,由组件自己 emit 事件来触发父级更新。
    4. 与 Composition API 的深度融合
    <script setup> 中的写法

    在 Vue 3 推荐的 <script setup> 语法里,v-model 依旧可以保留,只要配合 defineProps 与 defineEmits:

    <template>
      <input v-model="local" />
    </template>
    
    <script setup>
    const props = defineProps({ modelValue: String })
    const emit = defineEmits(['update:modelValue'])
    
    const local = ref(props.modelValue)
    watch(local, v => emit('update:modelValue', v))
    </script>

    父组件用 v-model 传进来,就完成了“父 → 子 → 父” 的单向数据流与事件流,清晰、安全。 

    而在纯模板层面,只需:

    <!-- 父组件 -->
    <CustomInput v-model="parentValue" />
    
    <!-- 子组件 -->
    <template>
      <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
    </template>
    
    <script setup>
    defineProps({ modelValue: String });
    defineEmits(['update:modelValue']);
    </script>
    
    useVModel 辅助函数

    社区常用的 @vueuse/core 提供 useVModel,进一步简化:

    <script setup>
    import { useVModel } from '@vueuse/core'
    
    const props = defineProps({
      modelValue: String
    })
    const emit = defineEmits(['update:modelValue'])
    
    const value = useVModel(props, 'modelValue', emit)
    // value 是一个 ref,与父组件 v-model 完全同步
    </script>
    5. 底层实现原理
    流程概览
    1. 编译模版阶段
      1. 解析到 v-model 指令
      2. 在 AST(抽象语法树)中添加相应的指令节点
      3. 将 v-model:foo="bar" 转换为标准的 v-bind:foo="bar" + v-on:update:foo="bar = $event"
    2. 渲染函数生成
      1. 把 AST 里的指令节点输出成 createVNode/h() 调用
      2. 在 vnode 上挂载 modelValue 属性和 onUpdate:modelValue 事件处理器
    3. 运行时执行
      1. 对于原生元素,用专门的 vModelXxx 指令模块(如 vModelText、vModelCheckbox)来完成 DOM → 数据 和 数据 → DOM 的双向绑定。
      2. 对于组件,直接以 prop/event 的形式读写,组件自己负责调用 emit('update:foo', newVal)。
    编译器层面:指令到渲染函数的演变

    1、AST 转换

    当解析到模版中的一条 v-model 指令时,Vue 3 的编译器(@vue/compiler-core)会在 AST 节点上做如下两步转换:

    <!-- 模板 -->
    <Child v-model:title="pageTitle" />
    
    1. 添加绑定
      1. 在节点的 props 中添加一条 modelValue: pageTitle(对应 :title="pageTitle")。
    2. 添加事件
      1. 在节点的事件监听中添加一条 onUpdate:title: $event => pageTitle = $event(对应 @update:title="pageTitle = $event")。

    2、代码生成

    最终,渲染函数里那一行 vnode 创建会变成:

    // 渲染函数伪代码
    return createVNode(Child, {
      title: pageTitle,
      'onUpdate:title': $event => (pageTitle = $event)
    });
    

    或者如果是原生 <input v-model="msg" />,则会调用:

    return withDirectives(
      createVNode('input', { value: msg }),
      [[vModelText, msg]]
    );

    这里 withDirectives 会把指令传给运行时去处理。

    运行时层面:专用指令与事件流

    Vue 3 在运行时提供了一组专用的 vModelXxx 指令函数,分别针对不同类型的表单控件:

    指令作用对象对应包与模块
    vModelText

    <input type="text">、<textarea>

    @vue/runtime-dom

    vModelCheckbox

    <input type="checkbox">

    @vue/runtime-dom

    vModelRadio

    <input type="radio">

    @vue/runtime-dom

    vModelSelect

    <select>

    @vue/runtime-dom

    1、指令内部核心流程

    以 vModelText 为例,伪代码运行时实现:

    // vModelText 指令主要钩子
    const vModelText = {
      // 在 mount 时设置初始值并注册事件
      mounted(el, binding, vnode) {
        el.value = binding.value; 
        el.addEventListener('input', e => {
          let newVal = e.target.value;
          // 处理 trim、number 修饰符……
          vnode.props['onUpdate:modelValue'](newVal);
        });
      },
      // 在更新时,同步最新数据回 DOM
      updated(el, binding) {
        if (binding.value !== el.value) {
          el.value = binding.value;
        }
      }
    };
    
    • mounted 阶段:将初始值写入 el.value,注册 input 事件,事件回调内调用 onUpdate:modelValue(即父级绑定的 $event => msg = $event)。
    • updated 阶段:在响应式更新阶段,将最新的 binding.value(即 msg)同步回 DOM,确保父改动也能反映在输入框上。

    2、事件流程图

    父组件 data.foo
           ↓     (渲染阶段)
    input.value = foo
           ↓     (用户输入)
      input事件 ⇒ 拦截 newVal
           ↓
    触发 onUpdate:modelValue(newVal)
           ↓
    foo = newVal(响应式 setter)
           ↓     (再次渲染)
    input.value = foo
    

    这个“数据→视图→事件→数据”循环就是双向绑定的核心。

    6. 扩展与自定义
    自定义组件的 v-model

    自定义组件只要 暴露 modelValue prop 和 update:modelValue 事件即可与 v-model 配合:

    <template>
      <input
        :value="modelValue"
        @input="$emit('update:modelValue', $event.target.value)"
      />
    </template>
    
    <script setup>
    defineProps({ modelValue: String });
    const emit = defineEmits(['update:modelValue']);
    </script>
    

    父级直接 <MyComp v-model="foo" />,毫无额外配置。

    修饰符管道

    Vue 3 将原生的 .trim / .number / .lazy 修饰符也统一在指令内部处理:

    // 示例:input 事件处理器内
    el.addEventListener(modifiers.lazy ? 'change' : 'input', e => {
      let val = e.target.value;
      if (modifiers.trim) val = val.trim();
      if (modifiers.number) val = Number(val);
      vnode.props['onUpdate:modelValue'](val);
    });
    

    这样在模板上写 <input v-model.number.trim="age" /> 就被自动组合成“数字转换 + 空白裁剪 + 延迟触发”的完整逻辑。

    四、Vue 3.4 的 defineModel

    1. 为什么需要 defineModel 呢?

    1、从 Vue 2.x 到 Vue 3.x 的双向绑定演进

    Vue 2.x 引入了 v-model,将表单输入的值与组件数据做双向绑定,语法糖极大简化了表单开发,但仍有痛点:只能同时支持一个 v-model,需要手写大量的 props 和 emits;多模型绑定依赖 .sync 或手动扩展,TS 场景下类型声明冗余。

    Vue 3.0 ~ 3.3 在底层改为 modelValue + update:modelValue、支持多 v-model:arg,解决了事件/prop 名冲突、支持多个模型。但在 <script setup> 下,开发者仍需显式写:

    const props = defineProps<{ modelValue: string }>()
    const emit = defineEmits<{'update:modelValue': (v: string) => void}>()

    样板代码较多,多模型下成倍膨胀。

    2、样板代码的维护成本

    在大型组件库或业务组件中,常见场景:提示框、表单字段、分页器都需要双向绑定。需要为每个字段定义对应的 prop 和 emit:

    defineProps({
      visible: Boolean,
      pageSize: Number,
      currentIndex: Number,
    });
    
    defineEmits([
      'update:visible',
      'update:pageSize',
      'update:currentIndex',
    ]);

    当字段越来越多,或者要做版本迭代、字段重命名,维护成本高、易出错。

    3、<script setup> + TypeScript 的机会

    Vue 3 推荐使用 <script setup>,它本身简化了生命周期、props、emits 的写法。但对 v-model 支持依然需要手动声明:

    <template>
      <input v-model="props.modelValue" />
    </template>
    
    <script setup lang="ts">
    const props = defineProps<{ modelValue: string }>()
    const emit = defineEmits<{
      (e: 'update:modelValue', v: string): void
    }>()
    </script>

    当要支持多个 v-model 时,还要多次重复,体验并不够理想。

    4、团队一致性与 DX(开发者体验)追求

    对于中大型团队和组件库,统一简单、少样板、类型安全的“双向绑定”方案是刚需。Vue 核心团队在 3.4 版推出 defineModel 编译宏,旨在一次性声明、自动生成对应的 props 和 emits,极大提升 <script setup> 下的 DX。

    2. 基本使用
    安装与开启
    • Vue CLI:升级 @vue/compiler-sfc 至 3.4+ 版本。
    • Vite:升级 @vitejs/plugin-vue 至支持 Vue 3.4 的版本。
    • 一般不用额外配置,默认 <script setup> 就可使用 defineModel。
    单模型绑定
    <template>
      <!-- 自动在 props 中注入 value,emits 注入 update:value -->
      <input v-model="value" placeholder="请输入" />
    </template>
    
    <script setup lang="ts">
    defineModel({
      value: {
        type: String,
        default: ''
      }
    });
    </script>
    

    等同于:

    const props = defineProps<{ value: string }>()
    const emit = defineEmits<{
      (e: 'update:value', v: string): void
    }>()
    const value = useVModel(props, 'value', emit)
    
    多模型绑定:一次性声明多个字段
    <template>
      <h1>{{ title }}</h1>
      <button @click="visible = !visible">{{ visible ? '隐藏' : '显示' }}</button>
      <button @click="count++">Count: {{ count }}</button>
    </template>
    
    <script setup lang="ts">
    defineModel({
      title: { type: String, default: '' },
      visible: { type: Boolean, default: false },
      count: { type: Number, default: 0 },
    });
    </script>
    

    自动在 props 中注入 { title, visible, count },在 emits 中注入 [ 'update:title','update:visible','update:count' ],并在 <script setup> 中提供同名 ref。

    结合 TS 类型:接口或泛型声明

    如果需要更精准地声明类型:

    interface ModelProps {
      name: string
      age: number
    }
    
    defineModel<ModelProps>({
      name: String,
      age: Number,
    });
    

    defineModel 可接受泛型,TypeScript 会推断 props.name 是 string,props.age 是 number。

    默认值与必填
    defineModel({
      count: { type: Number, required: true },
      label: { type: String, default: '按钮' }
    });
    • required: true 强制父组件必须传入。
    • default 在未传值时使用默认值。
    3. 底层原理:编译宏如何工作?
    什么是“编译宏”

    :一种在编译阶段对源代码做特殊识别和变换的语法。

    与运行时 API 的区别:宏调用在源码阶段展开,不会引入额外运行时代码,只在编译器中执行。

    Vue 3.4 把 defineModel 实现为一个 SFC(单文件组件)编译宏:它在处理 <script setup> 时被识别,然后注入标准的 defineProps 与 defineEmits。

    宏展开示例

    开发者写:

    <script setup lang="ts">
    defineModel({
      foo: String,
      count: { type: Number, default: 0 }
    });
    </script>
    

     在编译器处理时,等同于在顶层自动插入:

    const __props = defineProps({
      foo: String,
      count: { type: Number, default: 0 }
    });
    const __emit = defineEmits(['update:foo','update:count']);
    const foo = useVModel(__props, 'foo', __emit);
    const count = useVModel(__props, 'count', __emit);
    

    然后删除 defineModel 的原始调用,最终生成的渲染函数里只有上述标准调用,没有宏调用残留。

    宏实现原理概览

    defineModel 并非运行时代码,它是编译宏(Compiler Macro):

    1. 编译阶段识别:Vue 的模板编译器在遇到 defineModel({ ... }) 时会将其展开。
    2. 注入 defineProps / defineEmits:自动生成与每个 key 对应的 defineProps 和 defineEmits 调用。
    3. 生成本地 ref:在 <script setup> 内返回一个同名的 ref,其 .value 与父组件 v-model 值双向同步。
    4. 抹除宏调用:最终打包产物中,defineModel 调用被移除,只保留标准 JS。

    因此,defineModel 在开发时提供极佳 DX(开发体验),但不会给运行时增加负担。

    为何无运行时开销
    • 宏展开的代码和手写 defineProps / defineEmits 是等价的,运行时代码没有额外分支或 API。
    • 宏调用在打包阶段被移除,最终 bundle 里只剩标准 API 调用。
    • 不会增加执行路径长度或闭包开销。
    4. 与传统 v-model 对比
    维度Vue 3.3 及更早 v-modelVue 3.4 defineModel
    声明样板

    手动写 defineProps + defineEmits

    一次写 defineModel 即可
    多模型模板写多次 v-model:arg,props/emits 需匹配宏中一并声明,自动注入 props & emits
    类型推导需自己定义泛型宏支持泛型输入,IDE 自动推断
    底层成本只有标准 API,无额外开销宏在编译阶段展开,运行时无差异
    可读性props/emits 分散,零碎一处集中声明,后续使用更简洁
    修饰符支持

    Vue 3.4 保留 .trim、.number、.lazy,也可以在宏声明中传递 modelModifiers 到子组件:

    <MyComp v-model.trim.lazy="foo" />
    

    父级会传入 modelModifiers={ trim: true, lazy: true },子组件可在 defineProps({ modelModifiers: Object }) 中接收,并据此自定义指令逻辑。

    自定义事件 / prop 映射

    如果要做类似 Vue 2.x 的 model: { prop: 'checked', event: 'change' },在 Vue 3.4 里可以配合自定义组件直接:

    <script setup>
    defineModel({
      checked: Boolean
    }, { // 可选第二参数:事件/prop 映射
      propName: 'checked',
      eventName: 'change'
    });
    </script>
    

    注:此语法目前需依赖 RFC 或社区插件支持,核心思想是在宏中传入映射选项

    5. 注意事项

    1、仅限 <script setup>

    defineModel 宏只能在 <script setup> 中使用,普通 <script> 或组合式 setup() 中无法生效。

    2、编译阶段工具链要求

    需升级 @vue/compiler-sfc—@vitejs/plugin-vue 至 3.4+,旧版本不识别宏。

    3、无法在运行时动态声明

    宏展开时机在编译,运行时无法条件化调用。它只适合静态声明。

    4、调试困难

    由于宏调用最终被删除,打包后无法在运行时代码中看到 defineProps、defineEmits,需在源码或产物源码上进行追踪。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值