Vue v-model 各種綁定類型的深入解析
解開 v-model, 組件 model 與 Vuex store 綁定的疑惑
目錄
1. 前言
2. 快速複習 v-model
3. 組件內部使用 model 綁定外部 v-model 數值
- value 綁定 String, Number, Boolean 類型
- value 綁定 Array 類型
- value 綁定 Object 類型
- 特殊的綁定
4. 組件 v-model 綁定 Vuex store 數值
5. 綁定的常見問題
6. 後記
前言
Vue 最大的特色之一就是能輕易實現 雙向綁定,讓數據與元素能夠同步改變,也就是使用 v-model
。
從簡單的表單類型元件 input, selector, radio, checkbox …等等之類的,還是常見的組件,像是畫面跳出的訊息框、通知框 Dialog/ Modal,切換訊息內容的頁籤 Tabs/Navs、展開縮放的組件 Collapse/Accordion ,一定都會看到 v-model
的身影,常用 Vue 組件框架的朋友們想必都懂吧 😃。
而高度客製化的表單組件,我在之前的專案中有碰到,因為要跟後端 API 做串接,API 的參數值多數由物件或陣列組合而成的,加上表單需要有即時更新的機制,綁定的數值內容可能就會直接使用 物件或陣列 的類型,這就沒想像中的容易囉!
所以這篇文章,會以自定義/客製化組件為主,瞭解組件中內部開發的 model
屬性值,定義外部對接的 v-model="value"
中的 value 數值類型,讓不同的類型或特殊情況下都可以正確地同步資料。
當然,有些人可能會說,使用 Vuex 做狀態管理資料就行了,任何組件都能適用呀,不需要這麼麻煩去綁定複雜的數值。是的,後續也會提到相關的內容。
本篇文章會從簡單的數值,講到複雜的綁定實作,還會帶到一些實務應用。
快速複習 v-model
<input v-model="dataValue">
// ...等於
<input :value="dataValue" @input="dataValue = $event.target.value" >
<元素>觸發事件 input
後,就會改變 dataValue 數值;而 dataValue 被手動修改後,會直接改變<元素> props
value 數值,這就是所謂的雙向綁定。前者為<元素>觸發了特定事件而改變的,後者為直接改變<元素> props 數值的。
使用在自定義組件上時,把 <元素> 換成 <組件> 也是類似的概念。不過,還需要設定 model 屬性,內容值為 model: { prop: <string>, event: <string> }
。
組件內部使用 model 綁定外部 v-model 數值
外部 v-model 數值指的是客製化組件,像是 <my-component>
,給予其他頁面或高級組件使用,能繼續綁定 v-model 的 value
數值。
<my-component v-model="value"></my-component>
以下的說明都是客製化組件 <my-component>
內部的程式碼,如何製作的與應用方式。
value 綁定 String, Number, Boolean 類型
這是最常見得數據類型,以下範例以 String 做代表。
假設有個 value 數值,需要給外部組件做雙向綁定,這裡使用 computed
方法設置一個變數 valueSync 來同步 value ,以下為最簡潔的範例:
上面使用的方式我自稱為 computed get + set
來設置我們的變數值:
get()
取得父組件傳來的 value 數值直接賦予 valueSync ,每當 value 改變就會對應到 valueSync 的顯示數值set(value)
當子組件變動 valueSync 數值時,就會執行此函式去觸發 input 事件,也就是同步改變 value
以下為組件中 valueSync 的使用方式,假設有個按鈕可以讓輸入的數值變成英文大寫狀態:
這樣的方式就等於把 valueSync 當作 vue data 響應的數值,能夠隨意改變,又能同步外部的 value 數值了。
注意:
this.valueSync = 'xxx'
或v-model
元素綁定 valueSync 時,會間接使用computed
方法來同步 value 數值。- 可以直接使用 valueSync 來代替 value 做各種資料的變動囉。
value 綁定 Array 類型
曾經是否有想過把陣列的變數丟進 v-model
裡呢?
其實很多時候,都只是讓組件去改變我們的變數就好,隨著內部去變化的單向綁定概念而已。
假設我們有一組人名列表 value 數值是從外部傳進來的,這個組件是可以更改人們的名稱與年齡,還有可以移除他們。
上面的 data
valueSync 是用來代替 props
value 數值的,好讓你可以在組件中輕易地修改數值。
而 valueSync 的變動也要同步 value ,再傳遞到父組件上,需要讓每個會變動到的地方都觸發事件來做同步,以下製做一個函式 syncPropValue
來統一觸發:
不過,當外部有變動 call API 或其他的組件會去改動到這個變數 v-model
value 時,就是進入雙向綁定的需求了。
有兩種做法可以達到需求:
- 直接使用 Vuex store 重寫一次成為共享狀態值,好處讓狀態管理變輕鬆,壞處改寫很麻煩。
- 直接修改內部組件,使用
watch
方法監聽v-model
傳進來的數值,再同步內部數值,好處就是很快速。
話不多說直接用後者做法,使用 watch + deep
方法監聽 props
value 數值的改變,以下為範例:
- 監聽父組件傳來的 value,同步到內部組件的 valueSync
watch
方法加上deep
屬性,深度觀察 value 物件或陣列,就能監聽到父組件傳來的屬性值變化。- 為何不用
computed
方法,卻要用data
方法設置 valueSync 呢? - 由於它們擁有屬性值被參考同步的特性,只要 value 的屬性值有任何改變,
computed
方法是無法被監測到的,也就無法觸發computed get + set
方法。
我們可以使用 vue-devtools 來觀察屬性值的變化,UI 上看似有變,但 vue 響應事件是監聽不到任何的變動的。
watch
方法裡面使用了if (value !== valueSync) {valueSync = value}
,為了預防父組件重新定義 value 數值,屬性值被參考的特性就沒用了,所以需要手動執行同步。
接著我們來回顧下內部組件 valueSync 的改變行為:
2. 監聽內部組件的 valueSync ,觸發事件同步到父組件 value
當子組件有任何改變 valueSync 數值時
this.valueSync.splice()
,this.valueSync.push()
this.valueSync = []
- 組件內部使用元素
v-model
綁定 valeSync 的屬性值
以上都需要做 this.$emit('input', this.valueSync)
同步到父組件 props
value。
3. 統整結論
- 內部組件的改變是能夠預期的,所以,這裡使用同一個函式
syncPropValue()
來管理data
valueSync 的改變再同步到props
value。 - 外部組件任意改變
props
value,使用watch + deep
方法來管理它的任何變動,以同步到data
valueSync。 watch
方法中使用if (value !== valueSync) {valueSync = value}
,當兩邊不相等時,需要手動同步數值,其他的就交給原生 屬性值參考 自動同步吧!
value 綁定 Object 類型
這種方式比較少見,通常是在開發組件時的方向不夠明確,其實很多時候只要 Object 中的某幾項屬性值就好了,使用組件的 prop
來接收這些屬性值才是好的做法,縮小變動的範圍才是上策。
那麼如果屬性值有好幾個呢?
這樣的話,直接使用 v-bind .sync
Modifier 雙向綁定,代替 v-model 數值,而觸發事件更新的方式就是 this.$emit('update:prop', value)
,以下為範例,假設有一組人名物件的數值:
可以看到直接使用 computed
方法來做同步綁定,這就回到了文章的第一部分, value 綁定 String, Number, Boolean 類型,是不是會更簡單了呢?
以上範例較為簡單,但如果 name & year 有關聯性的話,會互相影響去顯示 UI 或是計算數值的話,建議就需要改成 watch
方法 + syncPropValue()
的解法囉!
特殊的綁定
以下是我遇到的綁定經驗與應用:
- value 類型是 String,valueSync 類型是 Object 的組合
- [父綁子]: 設置
data
valueSync 做初始化* +watch
value 賦予this.valueSync = xxx
- [子綁父]: 統一一個函式
syncPropValue()
觸發事件同步到 value
初始化*:可以在 created 鉤子函式中設置數值,或直接為 watch 鉤子函式加上 immediate: true 參數
2. value 與 valueSync 類型都是 String 等單一數值,但子父組件不需同步,特定狀態下才同步數值
- [父綁子]: 設置
data
valueSync +watch
value 賦予this.valueSync = xxx
- [子綁父]: 在特定狀態下,統一一個函式
syncPropValue()
觸發事件同步到 value
syncValue 也能被修改成 尚未改變的 value 數值,只要不在特定狀態下都行。
3. value 與 valueSync 類型都是 Object or Array,但子父組件不需同步,特定狀態下才同步數值
- [父綁子]: 設置
data
valueSync 做深拷貝* +watch
value 賦予this.valueSync = xxx
深拷貝 +deep: true
屬性 - [子綁父]: 在特定狀態下,統一一個函式
syncPropValue()
觸發事件做深拷貝同步到 value
深拷貝*:避免屬性值參考的拷貝特性,可以使用 lodash 套件 _.deepClone 來實作。
另外,valueSync 也能夠深拷貝 為尚未改變的 value 數值。
組件 v-model 綁定 Vuex store 數值
相信在中小型以上的專案,都會用 Vuex 管理共同狀態了吧,結合 vuex store 的應用,綁定 v-model 的方式,官方也提供了解法:
使用 computed get & set 的寫法,同時達到設置 Vuex 的需求,但如果是綁定 filters.name, filters.habit 呢?
相信有開發者會為了減少程式碼,在不知覺情況下,寫成以下方式,同樣地也能運作:
看似簡潔,但 filters 的屬性值是無法被 Vuex 偵測到的,同理在 vue-devtools 也觀察不到任何變動。所以,官方推薦的寫法還是有必要的,但又臭又長地複製 computed get & set 三次上去,誰不覺得惱人呀XD
最好的解法就是使用 vuex-map-fields ,簡化上面的寫法又能被觀察到:
這個套件把 computed get & set 的寫法都模組化了,所以能夠很簡潔地使用 mapFields 函式綁定 v-model,使用方法跟 Vuex 的 mapGetters 函式很相似,甚至它還有多層巢狀的綁定方式喔!
綁定的常見問題
假如你發現,外部或父組件正在改變 v-model value,但內部組件的 valueSync 卻沒有被變動,請執行以下檢查:
- 查看外部 v-model 綁定數值是否為物件的屬性值 (像是
filter.xxx
) - 是的話,需要看這變數是如何被變動的,如果是變動 Object,需要使用
Vue.set()
改變數值,如果是變動 Array,則需要使用Array.splice(), Array.push()
之類的方式去改變數值,請參考官方 Change Detection Caveats。 - 否的話,就是看內部組件 valueSync 與 value 是否有互相同步呢?
- 如果是使用
computed
方法,要確認數值的類型是否為 String、Number、Boolean,不是的話,請改用適當的做法去綁定與同步數值。 - 如果是使用
watch
方法,查看是否有打開 deep 屬性值,查看是否有為 value 做初始化數值,查看函數方法中是否有邏輯忽略了 valueSync 的同步數值。
假如你發現,內部組件 valueSync 被使用者操作正在改變,但外部或父組件的 v-model value 卻沒有被變動,請執行以下檢查:
- 數值的類型為 String、Number、Boolean,就檢查 valueSync 是否有觸發到正確的事件上
this.$emit('input', value)
。 - 數值的類型為 Array,就檢查 valueSync 的屬性值,是否有 input 表單之類的 v-model 綁定與事件處理
@input or @change
。建議需要統一一個函式syncPropValue()
去觸發事件處理,帶入參數為 valueSync。 - 數值的類型為 Object,跟類型為 Array 的處理方式一樣,但這裡建議只要取得 Object 的屬性值當作
props
傳進組件裡,再加上.sync
Modifier 的方式去同步單一的props
數值,會輕鬆很多。
後記
這一篇文章我寫的有點太久的,中間工作忙著都快忘了,不過也能補一些經驗分享的部分。
看到組件之間要共享變數的變化時,不知道該如何下手的,其實最快的就是用 Vuex store 方式就好,但這樣就會綁定組件之間的關係,只要組件一多的話,耦合性就會越大,甚至如果數值是 Object / Array ,改變的都是屬性值,就會很難 debug,影響的層級就會變大。
Vuex store 的使用時機,我認為是 view & view 之間會共享的參數,例如:使用者的權限狀態與資訊,navbar, sidebar, header, footer 的訊息之類的。
不使用 Vuex store 的方式,可以改使用組件的 prop
代替,就只要關心組件傳進來的數值變化就行了,後續導入一些輔助工具像是 Storybook 就會更方便許多。
# 感謝您的觀看與我一同學習,如果有喜歡的~
# 來個拍手掌聲或分享,就是您最好的回饋,謝謝啦😊
# 回饋問卷
# 20201230