最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue3中defineModel用法及常見問題完全指南

 更新時(shí)間:2026年02月27日 10:59:24   作者:wuliHD  
defineModel是Vue 3.3 引入、3.4 穩(wěn)定的一個(gè)編譯器宏,用于簡(jiǎn)化組件的雙向數(shù)據(jù)綁定實(shí)現(xiàn),它讓開發(fā)者能夠更輕松地創(chuàng)建支持v-model的組件,減少了樣板代碼,提高了開發(fā)效率,這篇文章主要介紹了Vue3中defineModel用法及常見問題的相關(guān)資料,需要的朋友可以參考下

概述

defineModel 是 Vue 3.3 引入、3.4 穩(wěn)定的一個(gè)編譯器宏,用于簡(jiǎn)化組件的雙向數(shù)據(jù)綁定實(shí)現(xiàn)。它讓開發(fā)者能夠更輕松地創(chuàng)建支持 v-model 的組件,減少了樣板代碼,提高了開發(fā)效率。

為什么需要 defineModel?

在 Vue 3.3 之前,實(shí)現(xiàn)一個(gè)支持 v-model 的組件需要:

// 傳統(tǒng)方式 - 需要定義 props 和 emits
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

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

而使用 defineModel 后:

// defineModel 方式 - 簡(jiǎn)潔明了
const modelValue = defineModel()

主要優(yōu)勢(shì)

  • 簡(jiǎn)化代碼:減少重復(fù)的樣板代碼
  • 類型安全:更好的 TypeScript 支持
  • 修飾符支持:內(nèi)置對(duì) v-model 修飾符的支持
  • 性能優(yōu)化:編譯時(shí)優(yōu)化,運(yùn)行時(shí)開銷更小

基礎(chǔ)概念

什么是編譯器宏?

defineModel 是一個(gè)編譯器宏,這意味著它會(huì)在構(gòu)建階段被 Vue 編譯器處理,轉(zhuǎn)換為相應(yīng)的運(yùn)行時(shí)代碼。編譯器宏只在 <script setup> 中使用,不需要導(dǎo)入。

雙向數(shù)據(jù)綁定原理

Vue 的 v-model 本質(zhì)上是語(yǔ)法糖:

<!-- 父組件使用 -->
<ChildComponent v-model="count" />

<!-- 等價(jià)于 -->
<ChildComponent
  :model-value="count"
  @update:model-value="newValue => count = newValue"
/>

defineModel 自動(dòng)處理這種 prop 和 emit 的配對(duì)關(guān)系。

基本用法

1. 簡(jiǎn)單的模型綁定

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

<template>
  <input
    v-model="modelValue"
    placeholder="輸入內(nèi)容..."
  />
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const message = ref('Hello World')
</script>

<template>
  <ChildComponent v-model="message" />
  <p>當(dāng)前值: {{ message }}</p>
</template>

2. 命名模型

<!-- 自定義模型名稱 -->
<script setup>
// 聲明名為 'title' 的模型
const title = defineModel('title')

// 聲明名為 'count' 的模型
const count = defineModel('count')
</script>

<template>
  <input v-model="title" placeholder="標(biāo)題" />
  <input v-model="count" type="number" placeholder="數(shù)量" />
</template>
<!-- 父組件中使用 -->
<script setup>
import { ref } from 'vue'
import CustomModelComponent from './CustomModelComponent.vue'

const postTitle = ref('')
const postCount = ref(1)
</script>

<template>
  <CustomModelComponent
    v-model:title="postTitle"
    v-model:count="postCount"
  />
  <p>標(biāo)題: {{ postTitle }}</p>
  <p>數(shù)量: {{ postCount }}</p>
</template>

3. 設(shè)置默認(rèn)值和類型

<script setup>
// 帶類型和默認(rèn)值的模型
const name = defineModel({
  type: String,
  default: '匿名用戶'
})

const age = defineModel({
  type: Number,
  default: 0
})

const isActive = defineModel({
  type: Boolean,
  default: false
})
</script>

<template>
  <div>
    <label>姓名: <input v-model="name" /></label>
    <label>年齡: <input v-model="age" type="number" /></label>
    <label>
      <input v-model="isActive" type="checkbox" />
      激活狀態(tài)
    </label>
  </div>
</template>

高級(jí)特性

1. 模型修飾符

defineModel 內(nèi)置對(duì) v-model 修飾符的支持:

<!-- TextInput.vue -->
<script setup>
// 獲取模型值和修飾符對(duì)象
const [modelValue, modelModifiers] = defineModel()

// 監(jiān)聽修飾符
watch(modelModifiers, (newModifiers) => {
  console.log('當(dāng)前修飾符:', newModifiers)
})

// 使用修飾符處理數(shù)據(jù)
const processedValue = computed({
  get: () => modelValue.value,
  set: (value) => {
    if (modelModifiers.trim) {
      value = value.trim()
    }
    if (modelModifiers.upper) {
      value = value.toUpperCase()
    }
    modelValue.value = value
  }
})
</script>

<template>
  <input v-model="processedValue" />
  <p>修飾符: {{ JSON.stringify(modelModifiers) }}</p>
</template>
<!-- 使用修飾符 -->
<script setup>
import TextInput from './TextInput.vue'
const text = ref('')
</script>

<template>
  <TextInput v-model.trim.upper="text" />
  <p>處理后的值: {{ text }}</p>
</template>

2. 自定義修飾符

<!-- NumberInput.vue -->
<script setup>
const [modelValue, modelModifiers] = defineModel({
  // 自定義 getter/setter
  get(value) {
    // 處理自定義修飾符
    if (modelModifiers.currency) {
      return new Intl.NumberFormat('zh-CN', {
        style: 'currency',
        currency: 'CNY'
      }).format(value)
    }
    if (modelModifiers.percentage) {
      return `${value}%`
    }
    return value
  },
  set(value) {
    // 清理格式化后的值
    if (modelModifiers.currency) {
      return parseFloat(value.replace(/[¥,]/g, ''))
    }
    if (modelModifiers.percentage) {
      return parseFloat(value.replace('%', ''))
    }
    return parseFloat(value) || 0
  }
})
</script>

<template>
  <input v-model="modelValue" />
</template>

3. 復(fù)雜對(duì)象模型

<!-- UserProfile.vue -->
<script setup>
// 復(fù)雜對(duì)象的模型綁定
const user = defineModel({
  type: Object,
  default: () => ({
    name: '',
    email: '',
    age: 0,
    avatar: ''
  })
})

// 計(jì)算屬性驗(yàn)證
const isFormValid = computed(() => {
  return user.value.name &&
         user.value.email &&
         user.value.email.includes('@') &&
         user.value.age > 0
})

// 方法
const resetForm = () => {
  user.value = {
    name: '',
    email: '',
    age: 0,
    avatar: ''
  }
}
</script>

<template>
  <div class="user-profile">
    <div class="form-group">
      <label>姓名:</label>
      <input v-model="user.name" />
    </div>

    <div class="form-group">
      <label>郵箱:</label>
      <input v-model="user.email" type="email" />
    </div>

    <div class="form-group">
      <label>年齡:</label>
      <input v-model="user.age" type="number" />
    </div>

    <div class="form-group">
      <label>頭像URL:</label>
      <input v-model="user.avatar" />
    </div>

    <button @click="resetForm">重置</button>
    <p v-if="!isFormValid" class="error">
      請(qǐng)?zhí)顚懲暾挠脩粜畔?
    </p>
  </div>
</template>

<style scoped>
.user-profile {
  max-width: 400px;
  margin: 0 auto;
}

.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
}

.form-group input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.error {
  color: red;
}
</style>

TypeScript 支持

1. 基本類型注解

<script setup lang="ts">
// 基本類型注解
const text = defineModel<string>()
const number = defineModel<number>()
const boolean = defineModel<boolean>()

// 數(shù)組類型
const items = defineModel<string[]>({ default: () => [] })

// 對(duì)象類型
interface User {
  id: number
  name: string
  email: string
}

const user = defineModel<User>({
  type: Object as PropType<User>,
  required: true
})
</script>

2. 復(fù)雜類型定義

<script setup lang="ts">
import type { PropType } from 'vue'

interface FormField {
  id: string
  label: string
  type: 'text' | 'email' | 'number' | 'textarea'
  value: string | number
  required?: boolean
  placeholder?: string
  validation?: {
    min?: number
    max?: number
    pattern?: string
  }
}

const formFields = defineModel<FormField[]>({
  type: Array as PropType<FormField[]>,
  default: () => [],
  // 自定義驗(yàn)證函數(shù)
  validator: (value: FormField[]) => {
    return Array.isArray(value) &&
           value.every(field => field.id && field.label && field.type)
  }
})

// 獲取修飾符的類型
const [modelValue, modelModifiers] = defineModel<string, {
  trim?: boolean
  uppercase?: boolean
  required?: boolean
}>()

// 類型安全的方法
const updateField = (index: number, value: string) => {
  if (formFields.value[index]) {
    formFields.value[index].value = value
  }
}
</script>

3. 泛型組件

<script setup lang="ts">
// 泛型模型組件
interface GenericModelProps<T> {
  value: T
  options?: T[]
}

// 使用泛型
const modelValue = defineModel<T>({
  type: [String, Number, Object, Array] as PropType<T>,
  required: true
})

// 類型推斷
type ModelType = typeof modelValue extends { value: infer T } ? T : never
</script>

實(shí)戰(zhàn)案例

案例1:自定義輸入組件庫(kù)

<!-- BaseInput.vue -->
<script setup lang="ts">
interface Props {
  type?: 'text' | 'email' | 'password' | 'number' | 'tel'
  placeholder?: string
  disabled?: boolean
  readonly?: boolean
  maxlength?: number
  minlength?: number
}

// 支持多種修飾符
const [modelValue, modelModifiers] = defineModel<string, {
  trim?: boolean
  uppercase?: boolean
  lowercase?: boolean
  number?: boolean
}>()

const props = withDefaults(defineProps<Props>(), {
  type: 'text',
  placeholder: '',
  disabled: false,
  readonly: false
})

const emit = defineEmits<{
  focus: [event: FocusEvent]
  blur: [event: FocusEvent]
  input: [event: Event]
  change: [event: Event]
}>()

// 處理修飾符
const processedValue = computed({
  get: () => {
    let value = modelValue.value || ''

    if (modelModifiers.uppercase) {
      value = value.toUpperCase()
    }
    if (modelModifiers.lowercase) {
      value = value.toLowerCase()
    }

    return value
  },
  set: (value: string) => {
    if (modelModifiers.trim) {
      value = value.trim()
    }
    if (modelModifiers.number) {
      value = value.replace(/[^\d.-]/g, '')
    }
    modelValue.value = value
  }
})

// 事件處理
const handleInput = (event: Event) => {
  const target = event.target as HTMLInputElement
  processedValue.value = target.value
  emit('input', event)
}

const handleChange = (event: Event) => {
  const target = event.target as HTMLInputElement
  processedValue.value = target.value
  emit('change', event)
}
</script>

<template>
  <div class="base-input">
    <input
      :type="type"
      :value="processedValue"
      :placeholder="placeholder"
      :disabled="disabled"
      :readonly="readonly"
      :maxlength="maxlength"
      :minlength="minlength"
      @input="handleInput"
      @change="handleChange"
      @focus="$emit('focus', $event)"
      @blur="$emit('blur', $event)"
      class="input-field"
    />
  </div>
</template>

<style scoped>
.base-input {
  position: relative;
  display: inline-block;
}

.input-field {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  transition: border-color 0.2s;
  box-sizing: border-box;
}

.input-field:focus {
  outline: none;
  border-color: #409eff;
}

.input-field:disabled {
  background-color: #f5f7fa;
  cursor: not-allowed;
}

.input-field:readonly {
  background-color: #f5f7fa;
}
</style>

案例2:可編輯表格組件

<!-- EditableTable.vue -->
<script setup lang="ts">
interface TableColumn {
  key: string
  title: string
  width?: string
  type?: 'text' | 'number' | 'select'
  options?: Array<{ label: string; value: any }>
  required?: boolean
}

interface TableRow {
  [key: string]: any
}

interface Props {
  columns: TableColumn[]
  data: TableRow[]
  editable?: boolean
  addable?: boolean
  deletable?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  editable: true,
  addable: true,
  deletable: true
})

// 雙向綁定表格數(shù)據(jù)
const tableData = defineModel<TableRow[]>('data', {
  type: Array as PropType<TableRow[]>,
  required: true
})

// 計(jì)算屬性
const hasData = computed(() => tableData.value && tableData.value.length > 0)

// 方法
const addRow = () => {
  const newRow: TableRow = {}
  props.columns.forEach(col => {
    newRow[col.key] = col.type === 'number' ? 0 : ''
  })
  tableData.value = [...tableData.value, newRow]
}

const deleteRow = (index: number) => {
  tableData.value = tableData.value.filter((_, i) => i !== index)
}

const updateCell = (rowIndex: number, columnKey: string, value: any) => {
  const newData = [...tableData.value]
  newData[rowIndex][columnKey] = value
  tableData.value = newData
}

// 驗(yàn)證
const validateRow = (row: TableRow): boolean => {
  return props.columns.every(col => {
    if (col.required && (!row[col.key] || row[col.key] === '')) {
      return false
    }
    return true
  })
}

const validateTable = (): boolean => {
  return tableData.value.every(row => validateRow(row))
}
</script>

<template>
  <div class="editable-table">
    <table class="table">
      <thead>
        <tr>
          <th v-for="column in columns" :key="column.key" :style="{ width: column.width }">
            {{ column.title }}
            <span v-if="column.required" class="required">*</span>
          </th>
          <th v-if="deletable" class="action-column">操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, rowIndex) in tableData" :key="rowIndex" :class="{ 'invalid-row': !validateRow(row) }">
          <td v-for="column in columns" :key="column.key">
            <template v-if="editable">
              <input
                v-if="column.type === 'text' || !column.type"
                v-model="row[column.key]"
                @input="updateCell(rowIndex, column.key, ($event.target as HTMLInputElement).value)"
                type="text"
                class="cell-input"
              />
              <input
                v-else-if="column.type === 'number'"
                v-model.number="row[column.key]"
                @input="updateCell(rowIndex, column.key, ($event.target as HTMLInputElement).value)"
                type="number"
                class="cell-input"
              />
              <select
                v-else-if="column.type === 'select'"
                v-model="row[column.key]"
                @change="updateCell(rowIndex, column.key, ($event.target as HTMLSelectElement).value)"
                class="cell-select"
              >
                <option v-for="option in column.options" :key="option.value" :value="option.value">
                  {{ option.label }}
                </option>
              </select>
            </template>
            <template v-else>
              {{ row[column.key] }}
            </template>
          </td>
          <td v-if="deletable" class="action-column">
            <button @click="deleteRow(rowIndex)" class="delete-btn">刪除</button>
          </td>
        </tr>
      </tbody>
    </table>

    <div v-if="addable" class="table-actions">
      <button @click="addRow" class="add-btn">添加行</button>
    </div>

    <div v-if="!validateTable()" class="validation-error">
      請(qǐng)?zhí)顚懰斜靥钭侄?
    </div>
  </div>
</template>

<style scoped>
.editable-table {
  width: 100%;
}

.table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}

.table th,
.table td {
  border: 1px solid #ebeef5;
  padding: 8px 12px;
  text-align: left;
}

.table th {
  background-color: #f5f7fa;
  font-weight: 600;
}

.required {
  color: #f56c6c;
}

.cell-input,
.cell-select {
  width: 100%;
  padding: 4px 8px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
}

.cell-input:focus,
.cell-select:focus {
  outline: none;
  border-color: #409eff;
}

.invalid-row {
  background-color: #fef0f0;
}

.action-column {
  width: 100px;
  text-align: center;
}

.delete-btn {
  padding: 4px 8px;
  background-color: #f56c6c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.delete-btn:hover {
  background-color: #f78989;
}

.table-actions {
  margin-bottom: 1rem;
}

.add-btn {
  padding: 8px 16px;
  background-color: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.add-btn:hover {
  background-color: #66b1ff;
}

.validation-error {
  color: #f56c6c;
  font-size: 14px;
  margin-top: 8px;
}
</style>

案例3:搜索篩選組件

<!-- SearchFilter.vue -->
<script setup lang="ts">
interface FilterOption {
  label: string
  value: any
  type?: 'text' | 'select' | 'date' | 'number'
  options?: Array<{ label: string; value: any }>
}

interface SearchFilterProps {
  filters: FilterOption[]
  placeholder?: string
  debounceTime?: number
}

const props = withDefaults(defineProps<SearchFilterProps>(), {
  placeholder: '搜索...',
  debounceTime: 300
})

// 搜索關(guān)鍵詞模型
const searchKeyword = defineModel<string>('keyword', {
  default: ''
})

// 篩選條件模型
const filterValues = defineModel<Record<string, any>>('filters', {
  default: () => ({})
})

// 搜索狀態(tài)
const isSearching = ref(false)

// 防抖搜索
const debouncedSearch = useDebounceFn(() => {
  isSearching.value = true
  setTimeout(() => {
    isSearching.value = false
  }, 500)
}, props.debounceTime)

// 監(jiān)聽搜索關(guān)鍵詞變化
watch(searchKeyword, () => {
  debouncedSearch()
})

// 監(jiān)聽篩選條件變化
watch(filterValues, () => {
  debouncedSearch()
}, { deep: true })

// 重置搜索
const resetFilters = () => {
  searchKeyword.value = ''
  Object.keys(filterValues.value).forEach(key => {
    filterValues.value[key] = ''
  })
}

// 活躍篩選數(shù)量
const activeFilterCount = computed(() => {
  return Object.values(filterValues.value).filter(value =>
    value !== '' && value !== null && value !== undefined
  ).length + (searchKeyword.value ? 1 : 0)
})
</script>

<template>
  <div class="search-filter">
    <!-- 搜索輸入框 -->
    <div class="search-input-wrapper">
      <div class="search-icon">
        <svg viewBox="0 0 24 24" width="16" height="16">
          <path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
        </svg>
      </div>
      <input
        v-model="searchKeyword"
        type="text"
        :placeholder="placeholder"
        class="search-input"
      />
      <div v-if="searchKeyword" class="clear-icon" @click="searchKeyword = ''">
        <svg viewBox="0 0 24 24" width="16" height="16">
          <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
        </svg>
      </div>
    </div>

    <!-- 篩選條件 -->
    <div v-if="filters.length > 0" class="filters-wrapper">
      <div v-for="filter in filters" :key="filter.label" class="filter-item">
        <label class="filter-label">{{ filter.label }}:</label>

        <template v-if="filter.type === 'select'">
          <select v-model="filterValues[filter.label]" class="filter-select">
            <option value="">全部</option>
            <option
              v-for="option in filter.options"
              :key="option.value"
              :value="option.value"
            >
              {{ option.label }}
            </option>
          </select>
        </template>

        <template v-else-if="filter.type === 'date'">
          <input
            v-model="filterValues[filter.label]"
            type="date"
            class="filter-input"
          />
        </template>

        <template v-else-if="filter.type === 'number'">
          <input
            v-model.number="filterValues[filter.label]"
            type="number"
            class="filter-input"
          />
        </template>

        <template v-else>
          <input
            v-model="filterValues[filter.label]"
            type="text"
            :placeholder="filter.label"
            class="filter-input"
          />
        </template>
      </div>
    </div>

    <!-- 操作按鈕 -->
    <div class="filter-actions">
      <div v-if="activeFilterCount > 0" class="active-filters">
        已選擇 {{ activeFilterCount }} 個(gè)篩選條件
      </div>
      <button
        v-if="searchKeyword || activeFilterCount > 0"
        @click="resetFilters"
        class="reset-btn"
      >
        重置
      </button>
    </div>

    <!-- 搜索狀態(tài)指示器 -->
    <div v-if="isSearching" class="searching-indicator">
      搜索中...
    </div>
  </div>
</template>

<style scoped>
.search-filter {
  background-color: #f8f9fa;
  padding: 16px;
  border-radius: 8px;
  margin-bottom: 16px;
}

.search-input-wrapper {
  position: relative;
  margin-bottom: 12px;
}

.search-icon,
.clear-icon {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  color: #6c757d;
}

.search-icon {
  left: 12px;
}

.clear-icon {
  right: 12px;
  cursor: pointer;
}

.clear-icon:hover {
  color: #495057;
}

.search-input {
  width: 100%;
  padding: 8px 40px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 14px;
}

.search-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.filters-wrapper {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-bottom: 12px;
}

.filter-item {
  display: flex;
  flex-direction: column;
}

.filter-label {
  font-size: 12px;
  color: #6c757d;
  margin-bottom: 4px;
}

.filter-input,
.filter-select {
  padding: 6px 8px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 14px;
}

.filter-input:focus,
.filter-select:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.filter-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.active-filters {
  font-size: 12px;
  color: #007bff;
}

.reset-btn {
  padding: 6px 12px;
  background-color: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
}

.reset-btn:hover {
  background-color: #5a6268;
}

.searching-indicator {
  margin-top: 8px;
  font-size: 12px;
  color: #007bff;
  text-align: center;
}
</style>

最佳實(shí)踐

1. 組件設(shè)計(jì)原則

<!-- ? 好的實(shí)踐:?jiǎn)我宦氊?zé) -->
<script setup>
// 每個(gè)組件只負(fù)責(zé)一個(gè)主要功能
const value = defineModel<string>()

// 簡(jiǎn)單的數(shù)據(jù)處理邏輯
const processedValue = computed({
  get: () => value.value?.trim() || '',
  set: (val) => value.value = val.trim()
})
</script>

<!-- ? 避免的實(shí)踐:過度復(fù)雜 -->
<script setup>
// 避免在同一個(gè)組件中定義太多模型
const model1 = defineModel('model1')
const model2 = defineModel('model2')
const model3 = defineModel('model3')
// ... 太多模型會(huì)導(dǎo)致組件難以維護(hù)
</script>

2. 類型安全

<!-- ? 推薦的做法:完整的類型定義 -->
<script setup lang="ts">
interface UserForm {
  name: string
  email: string
  age: number
  preferences: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

const userForm = defineModel<UserForm>({
  type: Object as PropType<UserForm>,
  required: true,
  validator: (value: UserForm) => {
    return value.name &&
           value.email.includes('@') &&
           value.age > 0
  }
})
</script>

<!-- ? 不推薦的做法:缺少類型約束 -->
<script setup>
// 缺少類型定義,容易出現(xiàn)運(yùn)行時(shí)錯(cuò)誤
const data = defineModel() // 類型為 unknown
</script>

3. 默認(rèn)值處理

<!-- ? 推薦的做法:合理的默認(rèn)值 -->
<script setup>
// 對(duì)于復(fù)雜對(duì)象,使用函數(shù)返回默認(rèn)值
const user = defineModel({
  type: Object,
  default: () => ({
    name: '',
    email: '',
    age: 0
  })
})

// 對(duì)于數(shù)組,使用空數(shù)組作為默認(rèn)值
const items = defineModel({
  type: Array,
  default: () => []
})
</script>

<!-- ? 避免的做法:可變引用的默認(rèn)值 -->
<script setup>
// 可能導(dǎo)致多個(gè)組件實(shí)例共享同一個(gè)對(duì)象引用
const user = defineModel({
  type: Object,
  default: { name: '', email: '' } // ? 錯(cuò)誤:共享引用
})
</script>

4. 驗(yàn)證和錯(cuò)誤處理

<script setup>
const email = defineModel<string>({
  type: String,
  required: true,
  validator: (value: string) => {
    // 自定義驗(yàn)證邏輯
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(value)
  }
})

// 錯(cuò)誤狀態(tài)管理
const errorMessage = computed(() => {
  if (email.value && !validator(email.value)) {
    return '請(qǐng)輸入有效的郵箱地址'
  }
  return ''
})

// 實(shí)時(shí)驗(yàn)證
const isValid = computed(() => {
  return !errorMessage.value
})
</script>

<template>
  <div class="form-group">
    <label>郵箱:</label>
    <input v-model="email" type="email" />
    <div v-if="errorMessage" class="error-message">
      {{ errorMessage }}
    </div>
  </div>
</template>

5. 性能優(yōu)化

<script setup>
// ? 使用計(jì)算屬性進(jìn)行復(fù)雜的數(shù)據(jù)處理
const [modelValue, modelModifiers] = defineModel<string>()

const processedValue = computed({
  get: () => {
    let value = modelValue.value || ''

    // 復(fù)雜的格式化邏輯
    if (modelModifiers.currency) {
      value = Number(value).toLocaleString('zh-CN', {
        style: 'currency',
        currency: 'CNY'
      })
    }

    return value
  },
  set: (value: string) => {
    // 處理用戶輸入
    if (modelModifiers.currency) {
      value = value.replace(/[¥,]/g, '')
    }

    modelValue.value = value
  }
})

// ? 避免在模板中進(jìn)行復(fù)雜計(jì)算
<template>
  <!-- 不推薦:在模板中進(jìn)行復(fù)雜計(jì)算 -->
  <input :value="formatCurrency(modelValue)" @input="handleInput" />

  <!-- 推薦:使用計(jì)算屬性 -->
  <input v-model="processedValue" />
</template>
</script>

常見問題

1. 為什么 defineModel 不工作?

問題:組件中使用 defineModel 但沒有響應(yīng)式效果

原因

  • Vue 版本低于 3.3
  • 沒有在 <script setup> 中使用
  • 父組件沒有正確綁定 v-model

解決方案

<!-- 確保使用 Vue 3.3+ -->
<script setup>
// ? 正確使用
const modelValue = defineModel()
</script>

<!-- 父組件正確綁定 -->
<ChildComponent v-model="data" />
<!-- 而不是 -->
<ChildComponent :model-value="data" /> <!-- ? 缺少雙向綁定 -->

2. 如何處理默認(rèn)值的同步問題?

問題:子組件設(shè)置了默認(rèn)值,但父組件沒有提供值時(shí)出現(xiàn)同步問題

解決方案

<script setup>
// ? 推薦做法:使用本地狀態(tài)管理默認(rèn)值
const props = defineProps({
  modelValue: String
})

const emit = defineEmits(['update:modelValue'])

// 使用計(jì)算屬性處理默認(rèn)值
const internalValue = computed({
  get: () => props.modelValue || '默認(rèn)值',
  set: (value) => emit('update:modelValue', value)
})

// 或者使用 defineModel 的 default 選項(xiàng)
const modelValue = defineModel({
  type: String,
  default: '默認(rèn)值'
})
</script>

3. TypeScript 類型推斷失敗

問題:TypeScript 無(wú)法正確推斷 defineModel 的類型

解決方案

<script setup lang="ts">
// ? 顯式類型注解
const text = defineModel<string>()

interface FormData {
  name: string
  email: string
}

const formData = defineModel<FormData>({
  type: Object as PropType<FormData>
})

// ? 使用泛型約束
const model = defineModel<T extends string | number ? T : string>({
  type: [String, Number] as PropType<T>
})
</script>

4. 修飾符不生效

問題:自定義修飾符沒有按預(yù)期工作

解決方案

<script setup>
// ? 正確使用修飾符
const [modelValue, modelModifiers] = defineModel({
  // 在 set 函數(shù)中處理修飾符
  set(value) {
    if (modelModifiers.trim) {
      return value.trim()
    }
    return value
  }
})
</script>

<!-- 使用時(shí)添加修飾符 -->
<ChildComponent v-model.trim="data" />

5. 如何避免無(wú)限更新循環(huán)?

問題:在 getter 和 setter 中設(shè)置值時(shí)導(dǎo)致無(wú)限循環(huán)

解決方案

<script setup>
const [modelValue, modelModifiers] = defineModel()

// ? 避免在 getter 中修改原值
const processedValue = computed({
  get: () => {
    // 只讀取,不修改
    let value = modelValue.value || ''

    if (modelModifiers.uppercase) {
      return value.toUpperCase()
    }

    return value
  },
  set: (value) => {
    // 在 setter 中進(jìn)行實(shí)際修改
    let finalValue = value

    if (modelModifiers.trim) {
      finalValue = value.trim()
    }

    // 直接設(shè)置,避免觸發(fā) getter
    modelValue.value = finalValue
  }
})
</script>

與傳統(tǒng)方式的對(duì)比

1. 代碼量對(duì)比

<!-- ? 傳統(tǒng)方式:需要大量樣板代碼 -->
<script setup>
const props = defineProps({
  modelValue: String,
  title: String,
  count: Number
})

const emit = defineEmits([
  'update:modelValue',
  'update:title',
  'update:count'
])

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

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

const countValue = computed({
  get: () => props.count,
  set: (value) => emit('update:count', value)
})
</script>

<!-- ? defineModel 方式:簡(jiǎn)潔明了 -->
<script setup>
const modelValue = defineModel()
const title = defineModel('title')
const count = defineModel('count')
</script>

2. 修飾符支持對(duì)比

<!-- ? 傳統(tǒng)方式:需要手動(dòng)處理修飾符 -->
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:modelValue'])

const value = computed({
  get: () => props.modelValue,
  set: (value) => {
    if (props.modelModifiers.trim) {
      value = value.trim()
    }
    emit('update:modelValue', value)
  }
})
</script>

<!-- ? defineModel 方式:內(nèi)置修飾符支持 -->
<script setup>
const [modelValue, modelModifiers] = defineModel({
  set(value) {
    if (modelModifiers.trim) {
      return value.trim()
    }
    return value
  }
})
</script>

3. TypeScript 支持對(duì)比

<!-- ? 傳統(tǒng)方式:類型推斷不夠直觀 -->
<script setup lang="ts">
interface Props {
  modelValue?: string
}

const props = defineProps<Props>()
const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const value = computed<string>({
  get: () => props.modelValue || '',
  set: (value) => emit('update:modelValue', value)
})
</script>

<!-- ? defineModel 方式:類型推斷更直接 -->
<script setup lang="ts">
const value = defineModel<string>({
  default: ''
})
</script>

總結(jié)

defineModel 是 Vue 3 生態(tài)系統(tǒng)中的一個(gè)重要改進(jìn),它顯著簡(jiǎn)化了組件雙向數(shù)據(jù)綁定的實(shí)現(xiàn)。通過本文檔的學(xué)習(xí),我們了解到:

核心優(yōu)勢(shì)

  1. 簡(jiǎn)化開發(fā):大幅減少樣板代碼,提高開發(fā)效率
  2. 類型安全:優(yōu)秀的 TypeScript 支持,減少運(yùn)行時(shí)錯(cuò)誤
  3. 性能優(yōu)化:編譯時(shí)優(yōu)化,運(yùn)行時(shí)開銷更小
  4. 功能豐富:內(nèi)置修飾符支持和復(fù)雜的配置選項(xiàng)

適用場(chǎng)景

  • 表單組件:輸入框、選擇器、日期選擇器等
  • 數(shù)據(jù)展示組件:需要編輯功能的數(shù)據(jù)表格
  • 配置面板:各種設(shè)置和配置界面
  • 搜索篩選:復(fù)雜的搜索和篩選組件

學(xué)習(xí)建議

  1. 從基礎(chǔ)開始:先掌握基本的 v-model 綁定
  2. 逐步深入:學(xué)習(xí)修飾符和高級(jí)配置
  3. 實(shí)踐項(xiàng)目:在實(shí)際項(xiàng)目中應(yīng)用 defineModel
  4. 對(duì)比學(xué)習(xí):了解傳統(tǒng)方式的差異,更好地理解 defineModel 的優(yōu)勢(shì)

注意事項(xiàng)

  • 確保 Vue 版本為 3.3 或更高
  • 必須在 <script setup> 中使用
  • 注意默認(rèn)值的處理方式
  • 合理使用 TypeScript 類型約束

defineModel 代表了 Vue.js 持續(xù)改進(jìn)組件開發(fā)體驗(yàn)的努力,掌握這一特性將幫助開發(fā)者構(gòu)建更簡(jiǎn)潔、更可維護(hù)的 Vue 應(yīng)用程序。

到此這篇關(guān)于Vue3中defineModel用法及常見問題完全指南的文章就介紹到這了,更多相關(guān)Vue3 defineModel用法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Vue項(xiàng)目中使用better-scroll實(shí)現(xiàn)一個(gè)輪播圖自動(dòng)播放功能

    Vue項(xiàng)目中使用better-scroll實(shí)現(xiàn)一個(gè)輪播圖自動(dòng)播放功能

    better-scroll是一個(gè)非常非常強(qiáng)大的第三方庫(kù) 在移動(dòng)端利用這個(gè)庫(kù) 不僅可以實(shí)現(xiàn)一個(gè)非常類似原生ScrollView的效果 也可以實(shí)現(xiàn)一個(gè)輪播圖的效果。這篇文章主要介紹了Vue項(xiàng)目中使用better-scroll實(shí)現(xiàn)一個(gè)輪播圖,需要的朋友可以參考下
    2018-12-12
  • vue3使用ref獲取dom結(jié)果為'null'的原因詳析

    vue3使用ref獲取dom結(jié)果為'null'的原因詳析

    這篇文章主要給大家介紹了關(guān)于vue3使用ref獲取dom結(jié)果為'null'的原因詳析,ref是Vue3中一個(gè)非常重要的功能,它可以用來(lái)獲取DOM節(jié)點(diǎn),從而實(shí)現(xiàn)對(duì)DOM節(jié)點(diǎn)的操作,需要的朋友可以參考下
    2023-07-07
  • 詳解Vue的鉤子函數(shù)(路由導(dǎo)航守衛(wèi)、keep-alive、生命周期鉤子)

    詳解Vue的鉤子函數(shù)(路由導(dǎo)航守衛(wèi)、keep-alive、生命周期鉤子)

    這篇文章主要介紹了詳解Vue的鉤子函數(shù)(路由導(dǎo)航守衛(wèi)、keep-alive、生命周期鉤子),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧
    2018-07-07
  • vue+element搭建后臺(tái)小總結(jié) el-dropdown下拉功能

    vue+element搭建后臺(tái)小總結(jié) el-dropdown下拉功能

    這篇文章主要為大家詳細(xì)介紹了vue+element搭建后臺(tái)小總結(jié),el-dropdown下拉功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-09-09
  • Vue給?elementUI?中的?this.$confirm、this.$alert、?this.$prompt添加按鈕加載效果

    Vue給?elementUI?中的?this.$confirm、this.$alert、?this.$promp

    這篇文章主要介紹了Vue給?elementUI?中的?this.$confirm、this.$alert、?this.$prompt添加按鈕的加載效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-07-07
  • vue3 中實(shí)現(xiàn) Element Plus 表格合并的方法

    vue3 中實(shí)現(xiàn) Element Plus 表格合并的方法

    在Vue3和Element Plus中實(shí)現(xiàn)表格合并需要使用span-method方法,本文給大家介紹vue3中實(shí)現(xiàn)Element Plus表格合并的方法,感興趣的朋友跟隨小編一起看看吧
    2025-09-09
  • Vue中commit和dispatch區(qū)別及用法辨析(最新)

    Vue中commit和dispatch區(qū)別及用法辨析(最新)

    在Vue中,commit和dispatch是兩個(gè)用于觸發(fā)Vuex store中的mutations和actions的方法,這篇文章主要介紹了Vue中commit和dispatch區(qū)別及其用法辨析,需要的朋友可以參考下
    2024-06-06
  • vue的axios請(qǐng)求改變content-type為form-data問題

    vue的axios請(qǐng)求改變content-type為form-data問題

    這篇文章主要介紹了vue的axios請(qǐng)求改變content-type為form-data問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-09-09
  • vue動(dòng)態(tài)添加行/刪除行的完整代碼示例

    vue動(dòng)態(tài)添加行/刪除行的完整代碼示例

    在開發(fā)中我們常常會(huì)碰到這種業(yè)務(wù),有一些數(shù)據(jù)需要在前端進(jìn)行刪除,這篇文章主要給大家介紹了關(guān)于vue動(dòng)態(tài)添加行/刪除行的相關(guān)資料,需要的朋友可以參考下
    2024-02-02
  • vue封裝axios的幾種方法

    vue封裝axios的幾種方法

    在vue中最常用的應(yīng)該就是axios了,這是一個(gè)很強(qiáng)大的處理ajax的庫(kù)。今天我就分享一下我是如何封裝axios的。axios的基本api不再贅述,提前安裝一下也不用我說了吧
    2021-06-06

最新評(píng)論

临湘市| 博白县| 开封县| 兴化市| 四会市| 凉城县| 凤庆县| 仁怀市| 和平区| 逊克县| 慈利县| 巴楚县| 泰州市| 寿光市| 常宁市| 确山县| 桂东县| 鲜城| 翁牛特旗| 威远县| 宜兰市| 神木县| 兴义市| 阳朔县| 黄冈市| 静宁县| 南川市| 托克逊县| 沙河市| 江孜县| 临西县| 丁青县| 静宁县| 涿州市| 南丰县| 盘锦市| 板桥市| 东光县| 沾益县| 涟水县| 长阳|