直接跳到內容

組件 v-model

基本用法

v-model 可以在組件上使用以實現雙向綁定。

從 Vue 3.4 開始,推薦的實現方式是使用 defineModel() 宏:

vue
<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>parent bound v-model is: {{ model }}</div>
</template>

父組件可以用 v-model 綁定一個值:

template
<!-- Parent.vue -->
<Child v-model="count" />

defineModel() 返回的值是一個 ref。它可以像其他 ref 一樣被訪問以及修改,不過它能起到在父組件和當前變量之間的雙向綁定的作用:

  • 它的 .value 和父組件的 v-model 的值同步;
  • 當它被子組件變更了,會觸發父組件綁定的值一起更新。

這意味著你也可以用 v-model 把這個 ref 綁定到一個原生 input 元素上,在提供相同的 v-model 用法的同時輕鬆包裝原生 input 元素:

vue
<script setup>
const model = defineModel()
</script>

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

演練場示例

底層機制

defineModel 是一個便利宏。編譯器將其展開為以下內容:

  • 一個名為 modelValue 的 prop,本地 ref 的值與其同步;
  • 一個名為 update:modelValue 的事件,當本地 ref 的值發生變更時觸發。

在 3.4 版本之前,你一般會按照如下的方式來實現上述相同的子組件:

vue
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

如你所見,這顯得冗長得多。然而,這樣寫有助於理解其底層機制。

因為 defineModel 聲明了一個 prop,你可以通過給 defineModel 傳遞選項,來聲明底層 prop 的選項:

js
// 使 v-model 必填
const model = defineModel({ required: true })

// 提供一個默認值
const model = defineModel({ default: 0 })

WARNING

如果為 defineModel prop 設置了一個 default 值且父組件沒有為該 prop 提供任何值,會導致父組件與子組件之間不同步。在下面的示例中,父組件的 myRef 是 undefined,而子組件的 model 是 1:

js
// 子組件:
const model = defineModel({ default: 1 })

// 父組件
const myRef = ref()
html
<Child v-model="myRef"></Child>

首先讓我們回憶一下 v-model 在原生元素上的用法:

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

在代碼背後,模板編譯器會對 v-model 進行更冗長的等價展開。因此上面的代碼其實等價於下面這段:

template
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

而當使用在一個組件上時,v-model 會被展開為如下的形式:

template
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

要讓這個例子實際工作起來,<CustomInput> 組件內部需要做兩件事:

  1. 將內部原生 <input> 元素的 value 屬性綁定到 modelValue prop
  2. 當原生的 input 事件觸發時,觸發一個攜帶了新值的 update:modelValue 自定義事件

這裡是相應的代碼:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

現在 v-model 可以在這個組件上正常工作了:

template
<CustomInput v-model="searchText" />

在演練場中嘗試一下

另一種在組件內實現 v-model 的方式是使用一個可寫的,同時具有 getter 和 setter 的 computed 屬性。get 方法需返回 modelValue prop,而 set 方法需觸發相應的事件:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

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

v-model 的參數

組件上的 v-model 也可以接受一個參數:

template
<MyComponent v-model:title="bookTitle" />

在子組件中,我們可以通過將字符串作為第一個參數傳遞給 defineModel() 來支持相應的參數:

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

<template>
  <input type="text" v-model="title" />
</template>

在演練場中嘗試一下

如果需要額外的 prop 選項,應該在 model 名稱之後傳遞:

js
const title = defineModel('title', { required: true })
3.4 之前的用法
vue
<!-- MyComponent.vue -->
<script setup>
defineProps({
  title: {
    required: true
  }
})
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

在演練場中嘗試一下

在這種情況下,子組件應該使用 title prop 和 update:title 事件來更新父組件的值,而非默認的 modelValue prop 和 update:modelValue 事件:

vue
<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

在演練場中嘗試一下

多個 v-model 綁定

利用剛才在 v-model 參數小節中學到的指定參數與事件名的技巧,我們可以在單個組件實例上創建多個 v-model 雙向綁定。

組件上的每一個 v-model 都會同步不同的 prop,而無需額外的選項:

template
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
vue
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

在演練場中嘗試一下

3.4 之前的用法
vue
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

在演練場中嘗試一下

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

在演練場中嘗試一下

處理 v-model 修飾符

在學習輸入綁定時,我們知道了 v-model 有一些內置的修飾符,例如 .trim.number.lazy。在某些場景下,你可能想要一個自定義組件的 v-model 支持自定義的修飾符。

我們來創建一個自定義的修飾符 capitalize,它會自動將 v-model 綁定輸入的字符串值第一個字母轉為大寫:

template
<MyComponent v-model.capitalize="myText" />

通過像這樣解構 defineModel() 的返回值,可以在子組件中訪問添加到組件 v-model 的修飾符:

vue
<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

為了能夠基於修飾符選擇性地調節值的讀取和寫入方式,我們可以給 defineModel() 傳入 getset 這兩個選項。這兩個選項在從模型引用中讀取或設置值時會接收到當前的值,並且它們都應該返回一個經過處理的新值。下面是一個例子,展示了如何利用 set 選項來應用 capitalize (首字母大寫) 修飾符:

vue
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

在演練場中嘗試一下

3.4 之前的用法
vue
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

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

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

在演練場中嘗試一下

新增至組件的 v-model 的修飾符將透過 modelModifiers 屬性提供給組件。在下面的範例中,我們建立了一個包含 modelModifiers 屬性的組件,該屬性預設為空物件:

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
}
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

請注意,組件的 modelModifiers 屬性包含 capitalize,值為 true ——因為它是在 v-model 綁定的 v-model.capitalize="myText" 上設定的。

現在我們已經為組件配置了 prop,我們可以檢查 modelModifiers 屬性並編寫一個處理程序來更改發出的值。在下面的代碼中,每當 <input /> 元素觸發 input 事件時,我們都會將字符串大寫。

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

在演練場中嘗試一下

帶參數的 v-model 修飾符

對於同時包含參數又有修飾符的 v-model 綁定,生成的 prop 名將是 arg + "Modifiers"。舉例來說:

template
<MyComponent v-model:title.capitalize="myText">

相應的聲明應該是:

js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}

這裡是另一個例子,展示了如何在使用多個不同參數的 v-model 時使用修飾符:

template
<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
3.4 之前的用法
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])

console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>
vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>
組件 v-model已經加載完畢