Vue3 元件通信:v-model 雙向綁定(父子元件)
簡介
在單頁應用程式中,父子元件之間的資料同步是最常見的需求之一。Vue 3 透過 v-model 為我們提供了「雙向綁定」的直覺寫法,讓父層可以直接控制子層的狀態,同時子層的變更也會自動回傳給父層。
除了最基礎的 v-model(綁定 modelValue + update:modelValue 事件),Vue 3 仍支援 多個 v-model、修飾符、以及 .sync 語法,讓開發者能以最少的樣板程式碼,建立可重用、易維護的 UI 元件。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 父子元件雙向綁定 的核心技巧,並提供實務應用情境,幫助你在專案中快速上手。
核心概念
1. v-model 的基本原理
在 Vue 3 中,v-model 其實是 語法糖,最終會被編譯成以下兩個操作:
<!-- 父層 -->
<my-input v-model="msg" />
會被轉譯為:
<my-input :modelValue="msg" @update:modelValue="val => msg = val" />
modelValue為子元件接受的 propupdate:modelValue為子元件發出的 事件,父層收到後更新本地資料
重點:子元件 不可直接修改
modelValue,必須透過$emit('update:modelValue', newValue)來回傳新值。
2. 建立最簡單的雙向綁定元件
範例 1:自訂文字輸入元件
<!-- MyInput.vue -->
<template>
<input
:value="modelValue"
@input="onInput"
placeholder="請輸入文字"
/>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
modelValue: String // 接收父層傳入的值
})
const emit = defineEmits(['update:modelValue'])
function onInput(event) {
// 透過 $emit 回傳新值給父層
emit('update:modelValue', event.target.value)
}
</script>
<!-- Parent.vue -->
<template>
<MyInput v-model="text" />
<p>你輸入的是:{{ text }}</p>
</template>
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
const text = ref('')
</script>
此範例展示 最小化 的雙向綁定流程,父層的
text會隨著子元件的輸入即時更新。
3. 自訂 v-model 的 Prop 與 Event 名稱
Vue 3 允許在 <child v-model:foo="bar" /> 的形式,自訂 綁定的 prop 與事件名稱,讓同一個元件可以同時支援多個雙向綁定。
範例 2:支援 v-model:checked 與 v-model:label
<!-- MySwitch.vue -->
<template>
<label>
<input type="checkbox"
:checked="checked"
@change="onCheckedChange" />
<span>{{ label }}</span>
</label>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
checked: Boolean, // 由 v-model:checked 綁定
label: String // 由 v-model:label 綁定
})
const emit = defineEmits(['update:checked', 'update:label'])
function onCheckedChange(e) {
emit('update:checked', e.target.checked)
}
</script>
<!-- Parent.vue -->
<template>
<MySwitch
v-model:checked="isOn"
v-model:label="title"
/>
<p>開關狀態:{{ isOn }},標題:{{ title }}</p>
</template>
<script setup>
import { ref } from 'vue'
import MySwitch from './MySwitch.vue'
const isOn = ref(false)
const title = ref('關閉')
</script>
技巧:在子元件內部,
checked、label仍然是 只讀 的 prop,若需要在子元件內部改變,仍須使用$emit。
4. 多個 v-model(同時綁定多個狀態)
從 Vue 3.2 起,v-model 支援 多個同時綁定,非常適合像 表單元件、日期範圍選擇器 等需要一次傳遞多個值的情境。
範例 3:日期範圍選擇器
<!-- DateRangePicker.vue -->
<template>
<div class="range-picker">
<input type="date"
:value="start"
@input="val => emit('update:start', val.target.value)" />
<span>~</span>
<input type="date"
:value="end"
@input="val => emit('update:end', val.target.value)" />
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
start: String,
end: String
})
const emit = defineEmits(['update:start', 'update:end'])
</script>
<!-- Parent.vue -->
<template>
<DateRangePicker
v-model:start="rangeStart"
v-model:end="rangeEnd"
/>
<p>選取區間:{{ rangeStart }} ~ {{ rangeEnd }}</p>
</template>
<script setup>
import { ref } from 'vue'
import DateRangePicker from './DateRangePicker.vue'
const rangeStart = ref('')
const rangeEnd = ref('')
</script>
說明:父層只需要在
v-model:start、v-model:end上分別指定對應的變數,子元件內部則使用update:start、update:end事件回傳。
5. 使用 .sync 修飾符(舊版寫法的兼容)
在 Vue 2 時,.sync 是常見的雙向綁定方式。Vue 3 仍保留此語法,以方便 從舊專案遷移。
<!-- Parent.vue -->
<template>
<MyInput :value.sync="msg" />
</template>
等同於:
<my-input :value="msg" @update:value="val => msg = val" />
提醒:
.sync只能在 單一 prop 上使用,若要同時綁定多個值,仍建議使用v-model:xxx。
6. v-model 修飾符:number、trim、lazy
Vue 內建三個常用的修飾符,用以在 資料傳遞前 自動處理類型或空白:
| 修飾符 | 功能 | 範例 |
|---|---|---|
.number |
將字串轉為數字 | <my-input v-model.number="age" /> |
.trim |
去除前後空白 | <my-input v-model.trim="name" /> |
.lazy |
改為 change 事件觸發 |
<my-input v-model.lazy="msg" /> |
在自訂元件中,修飾符會自動套用在
update:modelValue事件的 payload 上,開發者無需額外處理。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 直接變更 Prop | 子元件內部直接 props.modelValue = newVal 會觸發 Vue 警告,且不會同步回父層。 |
使用 $emit('update:modelValue', newVal),或在 <script setup> 中使用 defineEmits。 |
| 命名不一致 | v-model 預設對應 modelValue,若子元件改用 value,父層仍寫 v-model 會失效。 |
明確在子元件中宣告 modelValue,或在父層使用 v-model:customProp。 |
多個 v-model 事件衝突 |
同一個 prop 同時使用 v-model 與 .sync 會產生重複更新。 |
統一使用 v-model 系統,避免混用。 |
| 事件名稱拼寫錯誤 | update:modelValue 必須全小寫,若寫成 update:ModelValue 會無法觸發。 |
使用 IDE 自動補全或常量 UPDATE_MODEL_VALUE = 'update:modelValue'。 |
| 父層資料不是響應式 | 父層傳入的值若是普通變量(未使用 ref/reactive),子元件變更不會回傳。 |
確保父層變數為 響應式(ref、reactive、computed)。 |
最佳實踐
- 保持單向資料流:子元件只 發射 事件,絕不直接改變
props。 - 使用
defineProps&defineEmits:在<script setup>中明確宣告,讓 TypeScript 能正確推斷。 - 命名慣例:自訂
v-model時,建議使用 名詞(如checked、value)作為 prop,update:前綴作為事件。 - 避免過度綁定:若只需要單向傳遞,改用
props+$emit('change'),減少不必要的雙向綁定。 - 測試雙向綁定:使用
@vue/test-utils的setValue、trigger方法驗證父子同步行為。
實際應用場景
| 場景 | 為什麼適合使用 v-model |
|---|---|
表單元件庫(如自訂 <Select>、<DatePicker>) |
父層只需要寫 <MySelect v-model="selected">,即可自動同步選取結果。 |
| 彈窗/對話框(控制開關) | 父層透過 v-model:visible 控制顯示,子元件內部點擊「關閉」即 $emit('update:visible', false)。 |
| 可編輯表格(每列的編輯狀態) | 每列子元件 v-model:isEditing,父層維護編輯陣列,無需額外 watch。 |
| 多語系切換(切換語言後即時更新 UI) | 子元件使用 v-model:locale,父層改變時自動傳遞,子元件內部只負責渲染。 |
| 父子協同的圖表設定(如調整比例、顏色) | 父層保存設定物件,子元件 v-model:config,即時回傳變更,圖表即時重新渲染。 |
總結
v-model在 Vue 3 中是 雙向綁定的核心語法糖,背後等同於:modelValue+@update:modelValue。- 透過 自訂 prop/event 名稱(
v-model:foo)以及 多個v-model,我們可以在同一個子元件裡同時管理多筆狀態。 - 使用
.number、.trim、.lazy等修飾符,能在資料傳遞前自動完成常見的類型或字串處理。 - 避免直接改變 Prop、保持命名一致、確保父層變數是響應式,是避免常見錯誤的關鍵。
- 在實務開發中,
v-model是構建 可重用表單元件、彈窗控制、即時同步 UI 的最佳工具,只要遵守單向資料流的原則,就能寫出既簡潔又易維護的程式碼。
掌握了上述概念與技巧後,你就能在 Vue 3 專案中,輕鬆實作父子元件的雙向綁定,提升開發效率與程式品質。祝你寫程式快樂,元件溝通無礙!