iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 12

Day 12: v-model 的進化

  • 分享至 

  • xImage
  •  

v-model 的進化主要是為瞭解決雙向繫結的靈活性和可擴充套件性問題。

在新的版本中,v-model 不僅僅是用於表單元素的簡單雙向資料繫結,還支援跨元件的資料傳遞和管理。

這樣的改進允許開發者在自定義元件中更靈活地使用 v-model,並且可以輕鬆地定義和使用不同的繫結屬性,提升了元件之間的互操作性和可維護性。

v-model 的演進


  • Vue 3.0
    • 標準化為 modelValue / update:modelValue
    • 支援多個 v-modelv-model:titlev-model:visible
    • 修飾符(modifiers)也能用在元件上(例如 .trim),會落到對應的 xxxModifiers
  • Vue 3.4(關鍵)
    • 引入並穩定defineModel() :一行就做完 v-model 的 prop/emit,還能直接拿到修飾符、用 get/set 做轉換。
  • Vue 3.5(體驗升級)
    • v-model 本身語義 不變;但 3.5 把 可反應的 props 解構 等功能穩定化、核心 reactivity 做了大幅效能與記憶體優化,讓大表單 / 多層包裝元件更順暢。

小結:
Vue 3.5 的「進化」重點在於開發體驗、效能與周邊 API(像 props 解構),而 v-model 的 跨元件用法與 defineModel() 是在 3.4 奠基的。

v-model 底層怎麼運作?為什麼能做到這麼通用?


1. 原生表單

<input v-model="searchText" />

在底層中會被編譯成屬性綁定 + 事件回寫:

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

因為不同型別 input 會綁不同屬性 (如 checked)、不同事件 (change/input),.number / .trim / .lazy 等修飾符只是編譯時插入轉換邏輯。

2. 自訂元件

<MyInput v-model="msg" />

在底層中會被編譯:

<MyInput :model-value="msg" @update:model-value="v => (msg = v)" />

關鍵就是:

  • 子元件有 modelValue 這個 prop
  • 子元件在值變動時 emit('update:modelValue', newVal)

3. defineModel() 做了什麼

defineModel() 只是語法糖:它自動 宣告 modelValue propupdate:modelValue 事件,並回傳一個 可寫的 ref(你改它=向父層回寫)。

  • 可以用多個模型defineModel('title')defineModel('visible')
  • 可以解構出修飾符並在 set() / get() 轉換資料(例如 .trim)。
  • 也能給型別 / 預設值(注意:給了 default 但父層沒傳值時,父/子可能「不同步」)。

4. 更底層:為何資料一改就能推到畫面?

因為 Vue 的 Proxy 驅動響應式

get → 追蹤依賴(track),set → 觸發更新(trigger);模板被編譯成 render 函式,讀到 reactive 值就建立依賴,值變了就重新渲染對應片段。

v-model 不過是在這條鏈上加了一個「事件把值回寫」的環節,所以能無縫串接。

最後用一張流程圖來看整個觸發到畫面更新:

Vue 3.5 v-model 流程圖

常見細節與陷阱


1. 物件型 v-model 的「深層變更」

defineModel() 回來的是一個 淺層 ref,如果做 model.value.foo = 'x' 是直接改到父層物件,不是透過 emit
需要不可變或是審核流程時,建議在 set() 中回傳「複製後的新物件」,或在子層以淺拷貝後 emit

以下我們直接改物件屬性,這樣改 model.foo,其實是直接改到 父層物件本身 ,Vue 並不會透過 emit("update:modelValue") 傳出去,所以父層沒辦法攔截或審核。

<script setup lang="ts">
interface Model {
  foo: string
  bar: number
}

const model = defineModel<Model>()  // model.value 是一個 ref,指向父層物件
</script>

<template>
  <!-- 這裡直接改 foo -->
  <input v-model="model.foo" placeholder="直接改 foo" />
</template>

那該怎麼做會比較適合呢?

可以先複製後,再 emit

<script setup lang="ts">
interface Model {
  foo: string
  bar: number
}

const model = defineModel<Model>()

const updateFoo = (newFoo: string) => {
  model.value = { ...model.value, foo: newFoo }  // 用展開運算子做淺拷貝
}
</script>

<template>
  <input
    :value="model.foo"
    @input="updateFoo(($event.target as HTMLInputElement).value)"
    placeholder="安全更新 foo"
  />
</template>

2. default 可能造成不同步

子層因為 default 所以 model.value === 1

<script setup lang="ts">
const model = defineModel<number>({ default: 1 })
</script>

<template>
  <div>
    <p>子層拿到的值:{{ model }}</p>
    <button @click="model++">子層加一</button>
  </div>
</template>

引用的父層因為沒有初始化,所以 value === undefined,就會變成雙向綁定失敗。

<script setup lang="ts">
import Child from './ChildA.vue'
</script>

<template>
  <h2>父層值:{{ value }}</h2>
  <Child v-model="value" />
</template>

那該怎麼做會比較適合呢?

父層先初始化,也就是父子一開始都拿到 1 ,子層 +1,父層會同步更新。

<script setup lang="ts">
import { ref } from 'vue'
import Child from './ChildA.vue'

const value = ref(1)  // 父層初始化
</script>

<template>
  <h2>父層值:{{ value }}</h2>
  <Child v-model="value" />
</template>

3. Vue 3.5 的 props 解構

在包裝元件中,常會解 propswatch;Vue 3.5 起 解構後仍保留反應性,不用擔心失聯。

<script setup lang="ts">
interface Props {
  count: number
}

const { count } = defineProps<Props>()  // 直接解構也保留響應性
</script>

<template>
  <p>子層 count:{{ count }}</p>
</template>

Vue 3.5 後解構保留響應性,所以在父層點擊 +1,子層的 count 就會即時更新。

<script setup lang="ts">
import { ref } from 'vue'
import Child from './ChildA.vue'

const num = ref(0)
</script>

<template>
  <button @click="num++">+1</button>
  <Child :count="num" />
</template>

小結


因為 v-model 的本質是「受控元件(controlled component)」

單一真相來源在父層(或 store),資料向下用 propmodelValue),變更向上用事件(update:*)。
只要每一層都遵守這個協議,就能 一層層轉接 ,進而形成跨元件的資料流。

參考資料


  1. Vue.js - 組件 v-model
  2. v-model and child components?
  3. Component v-model
  4. Not sure how to properly approach defineModel with objects #10538

上一篇
Day 11: 跨元件的 provide / inject
系列文
Vue3.6 的革新:深入理解 Composition API12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言