vue3 checkbox使用v-model:value绑定reactive声明的值时响应式失效
参考下面的文章:
前言
在處理響應式多選表單時,使用 v-model 綁定陣列類型的資料,用 ref()
沒問題,但用 reactive()
卻行不通 QQ ~為什麼~~~
就是下面這個範例:
當初在響應式篇章,明明說 object 和 array 可以用 reactive()
來做響應的?
我就很開心的把 ref()
改成 reative()
了!
結果大踩坑,Vue 不會報錯或警告,但資料就是沒辦法響應。
這裡為懶得打開 Vue 開發環境的人做了個線上 DEMO 範例。
總之,我和讀書會夥伴就踏上了找尋真相的路途~!
在進入原始碼之前,想拋出個問題讓大家想想,這也會是等等研究原碼的重點。
『你有想過 v-model 是透過「改動原陣列」還是「創造新陣列」的方式來更新值嗎?』
v-model 是 Vue 提供的雙向綁定指令,在每次觸發事件時,會「更新」綁定的變數。
為什麼要強調「更新」?這跟 Javascript 的型別有關。
1 | const text = ref(""); |
1 | <input v-model="text" /> |
在進入原始碼之前,要不要先猜猜看,v-model 是透過「改動原陣列」還是「創造新陣列」的方式來更新值?
解讀 vModelCheckbox 原始碼
會針對 vModelCheckbox
-- v-model 綁定 checkbox 的部份做分析,完整原始碼連結
先看一下使用情境:
1 | const checkedNames = ref([]); |
1 | <div>Checked names: {{ checkedNames }}</div> |
vModelCheckbox 原始碼
1 | export const vModelCheckbox: ModelDirective<HTMLInputElement> = { |
補充說明:
looseIndex
為什麼不用原生的
indexOf
,是因為 Vue 支援 checkbox value 綁定非基本型別(如:物件),而原生 Javascript 方法只能判斷物件的參照是否相同,不能判斷物件內的值是否相同,所以要調用 Vue 自己定義的looseIndexOf
函式,來比較物件項目的內部值是否相同。
今日重點:v-model 如何「更新」陣列
前面有提到,要特別研究:v-model 如何處理「陣列型別 modelValue 的更新」
主要相關的程式碼是這段:
1 | //元素被打勾 && modelValue 內沒有 => 表示新打勾的 |
這裡調用的方法相信大家都不陌生,不管是新增或刪除,v-model 都不會直接更動原本的陣列(modelValue),而是創造一個指向不同參照的新陣列。
- 新增:
Array.prototype.concat()
會合併並回傳新陣列,不會影響原陣列- 刪除:
[...modelValue]
透過解構賦值展開,可以拿到一個新陣列- 再透過
Array.prototype.splice()
直接修改新陣列
也就是說,v-model 是透過重新賦值一個新陣列,來更新 modelValue 的值。
「更新」陣列和 ref
&reactive
的關係
大家還記得 ref
和 reactive
的特性嗎?
長話短說~reactive()
是利用 Proxy 來實作,所以 reactive
物件不能被重新賦值,這也是 v-model 多選綁定 reactive([])
會失效的原因。
註 1: RefImpl.valu
e 值指向物件依然可以被重新賦值
註 2: 還不清楚 ref
和 reactive
的特性的人可以參考之前的文章,這裡就不再贅述。
Day 10: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (上)
Day 11: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (下)
為什麼不支援 reactive
不能用 reactive()
做 v-model 陣列的響應,這難道不是個 bug 嗎?
不是
我們在 vue.js 的 github 上找到一串相關 issue。
vue.js 一度採納 issue 提案,讓 v-model 多選陣列能支援 reactive()
響應,但後來發現,這項變動的負面成本比預期的還大,所以幾乎是一天內又改回來了。
透過 reactive()
響應的資料不可被重新賦值,也就是說,如果 v-model 要支援 reactive
陣列,必須直接在 v-model 中改動(mutate) 綁定的陣列。
其中一個最大的影響就是,v-model + computed 的搭配用法會失效,modelValue 值的更新會繞過 computed 的 setter!
為什麼?
這樣操作會直接改動原本的陣列,而且陣列參照沒有改變,不會無法觸發 computed 的 setter。
v-model + computed 的搭配使用法參考如下:
現在的 Vue 是透過重新賦值來更新陣列,所以下面這個用法是沒問題的!
1 | <input v-model="inputValue" /> |
1 | const inputValue = computed({ |
如果要在現行 Vue 版本下模擬相似的情境,我覺得可以參考下面的程式碼,線上 DEMO 連結:
1 | const arr = reactive([]); |
1 | <button @click="computedArr.push("Jack")">push</button> |
我是用點擊觸發 computedArr.push("Jack")
模擬 v-model 直接操作 reactive
陣列的部份,每次 push
,原始的 arr
會直接被改動,而且每次改動陣列都無法觸發 computedArr 的 setter。
參考 issue: Checkbox v-model array mutation#2700
小結
- v-model 透過重新賦值來更新 modelValue(當型別為基本型別或陣列)
reactive()
物件不能重新賦值,陣列型別的 modelValue 只能透過ref()
來做響應- 不支援
reactive()
並不是 bug,是為了避免在 v-model 中改動原陣列,會導致非預期情況出現,負面效益大於正面效益
原文:Day 19: 你可能不知道的 v-model - 為何多選綁定陣列不能用 reactive()? - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天 (ithome.com.tw)