保姆式教你手写 Vue3 defineModel及底层原理分析

54 阅读8分钟

保姆式教你手写 Vue3 defineModel及底层原理分析

目录

  1. defineModel 概述
  2. 基本使用方法
  3. 底层原理分析
  4. 编译时转换机制
  5. 手写 defineModel 实现
  6. 实践案例
  7. 总结与思考

1. defineModel 概述

1.1 什么是 defineModel

defineModel 是 Vue 3.4+ 版本引入的一个编译器宏,用于简化组件双向数据绑定的实现。它是 v-model 指令的底层实现机制,提供了更直观、更简洁的 API。

1.2 历史背景

在 Vue 3.4 之前,实现组件的双向绑定需要:

  1. 使用 defineProps 定义接收的 prop
  2. 使用 defineEmits 定义触发的事件
  3. 手动处理 prop 的更新和事件的触发
// Vue 3.4 之前的写法
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const computedValue = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})

defineModel 的出现极大地简化了这个过程。

1.3 核心优势

  • 简化语法:一行代码替代多行模板代码
  • 类型安全:完整的 TypeScript 支持
  • 性能优化:编译时优化,运行时开销最小
  • 功能丰富:支持参数、修饰符等高级特性

2. 基本使用方法

2.1 基础用法

<!-- ChildComponent.vue -->
<script setup>
const modelValue = defineModel()
</script>

<template>
  <input v-model="modelValue" />
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const count = ref(0)
</script>

<template>
  <ChildComponent v-model="count" />
  <p>父组件的值: {{ count }}</p>
</template>

2.2 带参数的 defineModel

<!-- ChildComponent.vue -->
<script setup>
const title = defineModel('title')
const content = defineModel('content')
</script>

<template>
  <input v-model="title" placeholder="标题" />
  <textarea v-model="content" placeholder="内容"></textarea>
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const postTitle = ref('')
const postContent = ref('')
</script>

<template>
  <ChildComponent
    v-model:title="postTitle"
    v-model:content="postContent"
  />
</template>

2.3 带修饰符的 defineModel

<!-- ChildComponent.vue -->
<script setup>
// 支持内置修饰符
const modelValue = defineModel({
  set(value) {
    return value.trim() // 自动去除空格
  }
})

// 或者使用修饰符处理器
const modelValue = defineModel({
  // 处理 .number 修饰符
  number: true,
  // 处理 .trim 修饰符
  trim: true
})
</script>

2.4 自定义配置

<script setup>
const modelValue = defineModel({
  required: true,
  default: 'default value',
  validator: (value) => {
    return typeof value === 'string' && value.length > 0
  },
  // 本地修饰符处理器
  localModifiers: {
    capitalize: (value) => {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
  }
})
</script>

3. 底层原理分析

3.1 编译器宏机制

defineModel 本质上是一个编译器宏(Compiler Macro),它在编译阶段被转换为相应的 JavaScript 代码。

// 编译前
const model = defineModel()

// 编译后(简化版)
const props = __props
const emit = __emit
const model = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})

3.2 核心实现原理

3.2.1 编译时处理
  1. AST 分析:编译器解析 defineModel() 调用
  2. 依赖收集:分析函数参数和配置选项
  3. 代码生成:生成对应的 props、emits 和计算属性代码
3.2.2 运行时行为

编译后的代码在运行时的行为:

// 生成的代码结构
const __props = defineProps(['modelValue'])
const __emit = defineEmits(['update:modelValue'])

const model = computed({
  get: () => __props.modelValue,
  set: (value) => {
    // 处理修饰符
    if (modelModifiers.number) {
      value = toNumber(value)
    }
    if (modelModifiers.trim) {
      value = value.trim()
    }
    __emit('update:modelValue', value)
  }
})

3.3 类型系统

defineModel 提供了完整的 TypeScript 支持:

// 类型推导
const model = defineModel<string>() // 明确类型
const model = defineModel({ type: String }) // 运行时类型检查

// 复杂类型
interface User {
  id: number
  name: string
}
const user = defineModel<User>()

// 可选类型
const optionalModel = defineModel<string | null>()

4. 编译时转换机制

4.1 编译流程

源码 (.vue)
    ↓
模板编译 (Template Compiler)
    ↓
脚本编译 (Script Compiler)
    ↓
defineModel 处理 (Macro Expansion)
    ↓
代码生成 (Code Generation)
    ↓
最终 JavaScript 代码

4.2 详细转换过程

4.2.1 阶段一:词法分析

编译器识别 defineModel 标识符,并将其标记为宏调用。

4.2.2 阶段二:语法分析

构建 AST,分析 defineModel 的参数和配置:

// AST 节点示例
{
  type: 'CallExpression',
  callee: {
    type: 'Identifier',
    name: 'defineModel'
  },
  arguments: [],
  // 其他属性...
}
4.2.3 阶段三:语义分析
  1. 作用域检查:确保在 <script setup> 中使用
  2. 参数验证:检查参数类型和配置项
  3. 依赖分析:确定需要的 props 和 emits
4.2.4 阶段四:代码转换

生成对应的运行时代码:

// 原始代码
const model = defineModel('title', {
  required: true,
  default: '默认标题'
})

// 转换后的代码
const props = __props
const emit = __emit
const modelModifiers = __props.modelModifiers || {}

const model = computed({
  get: () => props.title,
  set: (value) => {
    if (modelModifiers.capitalize) {
      value = value.charAt(0).toUpperCase() + value.slice(1)
    }
    emit('update:title', value)
  }
})

4.3 优化策略

4.3.1 静态分析优化
  • 常量折叠:编译时计算常量表达式
  • 死代码消除:移除未使用的代码
  • 内联优化:小函数直接内联
4.3.2 运行时优化
  • 缓存机制:缓存计算属性结果
  • 批量更新:合并多个状态更新
  • 懒加载:延迟初始化大型对象

5. 手写 defineModel 实现

5.1 设计思路

要手写 defineModel,我们需要实现:

  1. 宏函数接口:提供与 defineModel 相同的 API
  2. 编译时处理:解析调用并生成对应代码
  3. 运行时支持:实现双向绑定的核心逻辑

5.2 简化版实现

5.2.1 宏定义
// utils/defineModel.js
export function createDefineModel() {
  // 存储模型定义的上下文
  const modelContext = new Map()

  return function defineModel(name = 'modelValue', options = {}) {
    // 获取当前组件上下文
    const context = getCurrentInstance()

    // 生成唯一的模型 ID
    const modelId = Symbol(`model_${name}`)

    // 定义 props
    const propName = name === 'modelValue' ? 'modelValue' : name
    const props = {
      [propName]: {
        type: options.type,
        required: options.required,
        default: options.default,
        validator: options.validator
      }
    }

    // 定义修饰符 props
    const modifiersProp = `${propName}Modifiers`
    props[modifiersProp] = {
      type: Object,
      default: () => ({})
    }

    // 定义 emit
    const emitName = name === 'modelValue' ? 'update:modelValue' : `update:${name}`

    // 创建计算属性
    const model = computed({
      get() {
        return context.props[propName]
      },
      set(value) {
        // 处理内置修饰符
        const modifiers = context.props[modifiersProp] || {}

        if (modifiers.number) {
          value = Number(value)
        }
        if (modifiers.trim) {
          value = String(value).trim()
        }
        if (modifiers.uppercase) {
          value = String(value).toUpperCase()
        }
        if (modifiers.lowercase) {
          value = String(value).toLowerCase()
        }

        // 执行自定义转换
        if (options.set) {
          value = options.set(value)
        }

        // 触发更新事件
        context.emit(emitName, value)
      }
    })

    // 存储模型信息
    modelContext.set(modelId, {
      name,
      props,
      emit: emitName,
      options
    })

    return model
  }
}
5.2.2 编译时转换器
// compiler/defineModelTransformer.js
export class DefineModelTransformer {
  constructor() {
    this.models = new Map()
  }

  transform(node, context) {
    if (!this.isDefineModelCall(node)) {
      return node
    }

    // 提取参数
    const { name, options } = this.extractArguments(node)

    // 生成变量名
    const modelVar = context.generateUid(`model_${name || 'value'}`)
    const propsVar = context.generateUid('props')
    const emitVar = context.generateUid('emit')

    // 生成代码
    const code = this.generateCode(name, options, modelVar, propsVar, emitVar)

    // 记录模型信息
    this.models.set(name, {
      variable: modelVar,
      props: this.generateProps(name, options),
      emits: this.generateEmits(name)
    })

    return code
  }

  generateCode(name, options, modelVar, propsVar, emitVar) {
    const propName = name === 'modelValue' ? 'modelValue' : name
    const emitName = name === 'modelValue' ? 'update:modelValue' : `update:${name}`
    const modifiersProp = `${propName}Modifiers`

    return `
const ${modelVar} = computed({
  get: () => ${propsVar}.${propName},
  set: (value) => {
    const modifiers = ${propsVar}.${modifiersProp} || {}

    // 处理内置修饰符
    if (modifiers.number) {
      value = Number(value)
    }
    if (modifiers.trim) {
      value = String(value).trim()
    }

    // 自定义转换
    ${options.set ? `value = (${options.set})(value)` : ''}

    ${emitVar}('${emitName}', value)
  }
})`
  }

  generateProps(name, options) {
    const propName = name === 'modelValue' ? 'modelValue' : name
    const modifiersProp = `${propName}Modifiers`

    const props = {
      [propName]: {
        type: options.type || null,
        required: options.required || false,
        default: options.default,
        validator: options.validator || null
      },
      [modifiersProp]: {
        type: Object,
        default: () => ({})
      }
    }

    return props
  }

  generateEmits(name) {
    const emitName = name === 'modelValue' ? 'update:modelValue' : `update:${name}`
    return [emitName]
  }
}

5.3 完整示例

5.3.1 核心实现
// myDefineModel.js
import { computed, getCurrentInstance } from 'vue'

export function createMyDefineModel() {
  return function myDefineModel(name = 'modelValue', options = {}) {
    const instance = getCurrentInstance()

    if (!instance) {
      throw new Error('myDefineModel must be called within setup()')
    }

    // 处理参数
    const modelName = name
    const propName = name === 'modelValue' ? 'modelValue' : name
    const modifiersProp = `${propName}Modifiers`
    const updateEvent = name === 'modelValue' ? 'update:modelValue' : `update:${name}`

    // 创建计算属性
    const model = computed({
      get() {
        return instance.props[propName]
      },
      set(value) {
        // 获取修饰符
        const modifiers = instance.props[modifiersProp] || {}

        // 应用修饰符
        value = applyModifiers(value, modifiers, options.localModifiers)

        // 应用自定义 setter
        if (options.set && typeof options.set === 'function') {
          value = options.set(value)
        }

        // 验证值
        if (options.validator && typeof options.validator === 'function') {
          if (!options.validator(value)) {
            console.warn(`Invalid value for model "${modelName}":`, value)
            return
          }
        }

        // 触发更新事件
        instance.emit(updateEvent, value)
      }
    })

    return model
  }
}

function applyModifiers(value, modifiers, localModifiers) {
  let result = value

  // 处理内置修饰符
  if (modifiers.number) {
    result = Number(result)
  }
  if (modifiers.trim) {
    result = String(result).trim()
  }
  if (modifiers.capitalize) {
    result = String(result).charAt(0).toUpperCase() + String(result).slice(1)
  }

  // 处理本地修饰符
  if (localModifiers) {
    Object.keys(modifiers).forEach(modifier => {
      if (localModifiers[modifier] && typeof localModifiers[modifier] === 'function') {
        result = localModifiers[modifier](result)
      }
    })
  }

  return result
}
5.3.2 使用示例
<!-- 使用自定义的 defineModel -->
<script setup>
import { createMyDefineModel } from './myDefineModel'

const defineModel = createMyDefineModel()

// 基础用法
const model = defineModel()

// 带配置
const title = defineModel('title', {
  required: true,
  default: '默认标题',
  validator: (value) => typeof value === 'string' && value.length > 0,
  localModifiers: {
    uppercase: (value) => String(value).toUpperCase()
  }
})

// 自定义 setter
const count = defineModel('count', {
  set: (value) => Math.max(0, Number(value)) // 确保非负数
})
</script>

<template>
  <div>
    <input v-model="model" placeholder="基础模型" />

    <input v-model="title" placeholder="标题模型" />

    <input v-model="count" type="number" placeholder="计数模型" />
  </div>
</template>

5.4 高级特性实现

5.4.1 类型推导支持
// types.ts
import { ComputedRef, InjectionKey } from 'vue'

export interface DefineModelOptions<T = any> {
  required?: boolean
  default?: T
  validator?: (value: T) => boolean
  set?: (value: T) => T
  localModifiers?: Record<string, (value: T) => T>
}

export interface ModelModifiers {
  number?: boolean
  trim?: boolean
  capitalize?: boolean
  [key: string]: any
}

export function defineModel<T = any>(
  name?: string,
  options?: DefineModelOptions<T>
): ComputedRef<T>

export function defineModel<T = any>(
  options?: DefineModelOptions<T>
): ComputedRef<T>
5.4.2 批量更新优化
// batchUpdates.js
const pendingUpdates = new Set()
let isFlushingUpdates = false

function scheduleUpdate(updateFn) {
  pendingUpdates.add(updateFn)

  if (!isFlushingUpdates) {
    isFlushingUpdates = true
    Promise.resolve().then(flushUpdates)
  }
}

function flushUpdates() {
  const updates = Array.from(pendingUpdates)
  pendingUpdates.clear()

  updates.forEach(update => update())
  isFlushingUpdates = false
}

// 在 defineModel 中使用
function createOptimizedModel(instance, propName, updateEvent) {
  return computed({
    get: () => instance.props[propName],
    set: (value) => {
      scheduleUpdate(() => {
        instance.emit(updateEvent, value)
      })
    }
  })
}

6. 实践案例

6.1 自定义输入组件

<!-- CustomInput.vue -->
<script setup>
const modelValue = defineModel({
  required: true,
  validator: (value) => typeof value === 'string',
  localModifiers: {
    reverse: (value) => value.split('').reverse().join('')
  }
})

const isFocused = ref(false)

const displayValue = computed({
  get: () => modelValue.value,
  set: (value) => {
    // 处理反向输入修饰符
    if (modelValueModifiers.reverse) {
      value = value.split('').reverse().join('')
    }
    modelValue.value = value
  }
})

function handleFocus() {
  isFocused.value = true
}

function handleBlur() {
  isFocused.value = false
}
</script>

<template>
  <div class="custom-input" :class="{ focused: isFocused }">
    <label>自定义输入框</label>
    <input
      v-model="displayValue"
      @focus="handleFocus"
      @blur="handleBlur"
      placeholder="请输入内容"
    />
    <div class="debug">
      当前值: {{ modelValue }}
      <br>
      修饰符: {{ $attrs.modelModifiers }}
    </div>
  </div>
</template>

<style scoped>
.custom-input {
  border: 2px solid #ccc;
  padding: 10px;
  border-radius: 4px;
  transition: border-color 0.3s;
}

.custom-input.focused {
  border-color: #42b883;
}

input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 2px;
}

.debug {
  margin-top: 10px;
  font-size: 12px;
  color: #666;
}
</style>

6.2 表单组件集合

<!-- FormField.vue -->
<script setup>
const props = defineProps({
  label: String,
  type: {
    type: String,
    default: 'text'
  },
  placeholder: String
})

const modelValue = defineModel({
  required: true,
  validator: (value) => {
    switch (props.type) {
      case 'email':
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
      case 'tel':
        return /^[\d\s\-\+\(\)]+$/.test(value)
      default:
        return true
    }
  }
})

const inputRef = ref(null)
const error = ref('')
const isValid = computed(() => !error.value)

function validate() {
  if (props.required && !modelValue.value) {
    error.value = `${props.label}是必填项`
    return false
  }

  if (props.type === 'email' && modelValue.value) {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(modelValue.value)) {
      error.value = '请输入有效的邮箱地址'
      return false
    }
  }

  error.value = ''
  return true
}

function handleInput() {
  error.value = ''
}

function handleBlur() {
  validate()
}

defineExpose({
  validate,
  isValid,
  focus: () => inputRef.value?.focus()
})
</script>

<template>
  <div class="form-field" :class="{ 'has-error': error }">
    <label>{{ label }}</label>
    <input
      ref="inputRef"
      :type="type"
      :placeholder="placeholder"
      v-model="modelValue"
      @input="handleInput"
      @blur="handleBlur"
    />
    <div v-if="error" class="error-message">{{ error }}</div>
  </div>
</template>

<style scoped>
.form-field {
  margin-bottom: 16px;
}

label {
  display: block;
  margin-bottom: 4px;
  font-weight: 500;
}

input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  transition: border-color 0.2s;
}

input:focus {
  outline: none;
  border-color: #42b883;
}

.form-field.has-error input {
  border-color: #f56565;
}

.error-message {
  color: #f56565;
  font-size: 12px;
  margin-top: 4px;
}
</style>

6.3 复杂数据结构组件

<!-- DataTable.vue -->
<script setup>
interface TableColumn {
  key: string
  label: string
  sortable?: boolean
  width?: string
}

interface TableData {
  [key: string]: any
}

const props = defineProps<{
  columns: TableColumn[]
  data: TableData[]
}>()

const selectedRows = defineModel<number[]>({
  default: () => []
})

const sortKey = defineModel<string>('sortKey')
const sortOrder = defineModel<'asc' | 'desc'>('sortOrder', { default: 'asc' })

const sortedData = computed(() => {
  if (!sortKey.value) return props.data

  return [...props.data].sort((a, b) => {
    const aValue = a[sortKey.value]
    const bValue = b[sortKey.value]

    if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1
    if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1
    return 0
  })
})

function handleSort(column: TableColumn) {
  if (!column.sortable) return

  if (sortKey.value === column.key) {
    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
  } else {
    sortKey.value = column.key
    sortOrder.value = 'asc'
  }
}

function handleSelectAll(event: Event) {
  const checked = (event.target as HTMLInputElement).checked
  if (checked) {
    selectedRows.value = sortedData.value.map((_, index) => index)
  } else {
    selectedRows.value = []
  }
}

function handleSelectRow(index: number, checked: boolean) {
  if (checked) {
    if (!selectedRows.value.includes(index)) {
      selectedRows.value.push(index)
    }
  } else {
    const idx = selectedRows.value.indexOf(index)
    if (idx > -1) {
      selectedRows.value.splice(idx, 1)
    }
  }
}
</script>

<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <th>
            <input
              type="checkbox"
              :checked="selectedRows.length === sortedData.length"
              :indeterminate="selectedRows.length > 0 && selectedRows.length < sortedData.length"
              @change="handleSelectAll"
            />
          </th>
          <th
            v-for="column in columns"
            :key="column.key"
            :style="{ width: column.width }"
            :class="{ sortable: column.sortable }"
            @click="handleSort(column)"
          >
            {{ column.label }}
            <span
              v-if="column.sortable && sortKey === column.key"
              class="sort-indicator"
            >
              {{ sortOrder === 'asc' ? '↑' : '↓' }}
            </span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, index) in sortedData" :key="index">
          <td>
            <input
              type="checkbox"
              :checked="selectedRows.includes(index)"
              @change="handleSelectRow(index, $event.target.checked)"
            />
          </td>
          <td v-for="column in columns" :key="column.key">
            {{ row[column.key] }}
          </td>
        </tr>
      </tbody>
    </table>

    <div class="table-info">
      已选择 {{ selectedRows.length }} 项,共 {{ sortedData.length }} 项
    </div>
  </div>
</template>

<style scoped>
.data-table {
  border: 1px solid #ddd;
  border-radius: 4px;
  overflow: hidden;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #eee;
}

th {
  background-color: #f8f9fa;
  font-weight: 600;
}

.sortable {
  cursor: pointer;
  user-select: none;
}

.sortable:hover {
  background-color: #e9ecef;
}

.sort-indicator {
  margin-left: 8px;
  color: #42b883;
}

.table-info {
  padding: 12px;
  background-color: #f8f9fa;
  border-top: 1px solid #ddd;
  font-size: 14px;
  color: #666;
}
</style>

7. 总结与思考

7.1 核心要点总结

  1. defineModel 是编译器宏:它在编译时被转换,不是运行时函数
  2. 基于计算属性实现:底层使用 computed 实现双向绑定
  3. 自动生成 props 和 emits:编译器自动生成必要的 props 定义和事件声明
  4. 支持修饰符系统:内置和自定义修饰符都可以处理
  5. 完整的 TypeScript 支持:提供类型安全和智能提示

7.2 设计模式分析

7.2.1 宏模式(Macro Pattern)
// 宏模式的特点
// 1. 编译时展开
// 2. 代码生成
// 3. 语法糖简化
defineModel() → computed() + props + emits
7.2.2 委托模式(Delegation Pattern)
// defineModel 委托给底层机制
const model = computed({
  get: () => props.value,     // 委托给 props
  set: (value) => emit('update:value', value)  // 委托给 emit
})
7.2.3 装饰器模式(Decorator Pattern)
// 修饰符系统类似于装饰器
value = applyModifiers(value, {
  number: true,    // 装饰:转换为数字
  trim: true,      // 装饰:去除空格
  capitalize: true  // 装饰:首字母大写
})

7.3 性能考虑

7.3.1 编译时优化
  • 常量折叠:编译时计算常量
  • 死代码消除:移除未使用的代码
  • 内联优化:小函数直接内联
7.3.2 运行时优化
  • 计算属性缓存:避免不必要的重新计算
  • 批量更新:合并多个状态更新
  • 惰性求值:按需计算值

7.4 最佳实践建议

  1. 合理使用 defineModel:适合简单的双向绑定,复杂逻辑考虑手动实现
  2. 善用修饰符:内置修饰符满足大多数需求,自定义修饰符处理特殊场景
  3. 注意类型安全:充分利用 TypeScript 的类型检查
  4. 性能优化:避免在 getter/setter 中执行耗时操作
  5. 测试覆盖:为复杂的 defineModel 配置编写单元测试

7.5 未来发展方向

  1. 更强的类型推导:更精确的 TypeScript 类型推断
  2. 更多内置修饰符:添加更多常用的数据处理修饰符
  3. 调试工具支持:更好的开发者工具集成
  4. 性能进一步优化:减少运行时开销
  5. 与组合式 API 更深度集成:与 Vue 3 生态系统更好融合

7.6 学习建议

对于学习 Vue 3 和 defineModel,建议按以下路径:

  1. 基础概念:先理解 Vue 的响应式系统和组件通信
  2. 传统方式:学习 props + emits 的双向绑定实现
  3. defineModel 语法:掌握 defineModel 的基本用法
  4. 原理理解:深入学习编译时转换机制
  5. 实践应用:在实际项目中使用 defineModel
  6. 源码阅读:阅读 Vue 3 源码,理解实现细节
  7. 手写实现:尝试实现简化版的 defineModel

通过这样的学习路径,可以深入理解 defineModel 的设计思想和实现原理,为后续的 Vue 开发打下坚实基础。


本文档基于 Vue 3.4+ 版本编写,部分特性可能在不同版本中有差异。建议在实际使用时参考对应版本的官方文档。