v-model
的進化主要是為瞭解決雙向繫結的靈活性和可擴充套件性問題。
在新的版本中,v-model
不僅僅是用於表單元素的簡單雙向資料繫結,還支援跨元件的資料傳遞和管理。
這樣的改進允許開發者在自定義元件中更靈活地使用 v-model
,並且可以輕鬆地定義和使用不同的繫結屬性,提升了元件之間的互操作性和可維護性。
v-model
的演進modelValue
/ update:modelValue
。v-model:title
、v-model:visible
。.trim
),會落到對應的 xxxModifiers
。defineModel()
:一行就做完 v-model 的 prop/emit,還能直接拿到修飾符、用 get/set
做轉換。v-model
本身語義 不變;但 3.5 把 可反應的 props 解構 等功能穩定化、核心 reactivity 做了大幅效能與記憶體優化,讓大表單 / 多層包裝元件更順暢。小結:
Vue 3.5 的「進化」重點在於開發體驗、效能與周邊 API(像 props 解構),而 v-model 的 跨元件用法與defineModel()
是在 3.4 奠基的。
v-model
底層怎麼運作?為什麼能做到這麼通用?<input v-model="searchText" />
在底層中會被編譯成屬性綁定 + 事件回寫:
<input :value="searchText" @input="searchText = $event.target.value" />
因為不同型別 input
會綁不同屬性 (如 checked
)、不同事件 (change/input
),.number
/ .trim
/ .lazy
等修飾符只是編譯時插入轉換邏輯。
<MyInput v-model="msg" />
在底層中會被編譯:
<MyInput :model-value="msg" @update:model-value="v => (msg = v)" />
關鍵就是:
modelValue
這個 prop
;emit('update:modelValue', newVal)
。defineModel()
做了什麼defineModel()
只是語法糖:它自動 宣告 modelValue
prop 與 update:modelValue
事件,並回傳一個 可寫的 ref(你改它=向父層回寫)。
defineModel('title')
、defineModel('visible')
。set()
/ get()
轉換資料(例如 .trim
)。default
但父層沒傳值時,父/子可能「不同步」)。因為 Vue 的 Proxy 驅動響應式!
get
→ 追蹤依賴(track),set
→ 觸發更新(trigger);模板被編譯成 render 函式,讀到 reactive
值就建立依賴,值變了就重新渲染對應片段。
v-model
不過是在這條鏈上加了一個「事件把值回寫」的環節,所以能無縫串接。
最後用一張流程圖來看整個觸發到畫面更新:
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>
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>
在包裝元件中,常會解 props
再 watch
;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),資料向下用
prop
(modelValue
),變更向上用事件(update:*
)。
只要每一層都遵守這個協議,就能 一層層轉接 ,進而形成跨元件的資料流。
v-model