Vue 3 新世界 defineModel(),你值得更好的開發體驗

更新於 發佈於 閱讀時間約 15 分鐘

前言

各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。

在這篇文章我們並不會探討 Vue 2 與 Vue 3 是如何實現雙向綁定,單向資料流等等的知識 (或許未來有一天可以來聊聊)。本篇我們著重於從 Vue 2 到 Vue 3 一路走來,實作雙向綁定的開發體驗歷程,以及 Vue 3.4 版本後新增的重磅巨集 defineModel()

SSS 級神卡 defineModel()

SSS 級神卡 defineModel()

Vue 2 時期

首先,來回顧一下雙向綁定資料在 Vue 2 時的開發體驗為何?眾所周知,Vue 採用單向資料流作為組件間的資料傳遞方式。
當我們希望一筆資料從父層組件傳遞給子層組件後,子層異動該筆資料時能夠實時地將異動內容更新回父層。這時我們就會分別建立父傳子與子傳父兩個資料傳遞動作,藉由分別建立兩個單向資料流,組合成能夠即時更新的跨組件雙向綁定。


使用 v-model 語法糖

// 父層組件
<template>
<div>
<ChildComponent v-model="foo" />
</div>
</template>

<script>
export default {
data() {
return {
foo: ''
}
}
}
</script>
// 子層組件
<template>
<div>
<input :value="value" @input="updateValue" />
</div>
</template>

<script>
export default {
props: ['value'],
methods: {
updateValue(event) {
this.$emit('input', event.target.value)
},
},
}
</script>


使用 .sync + computed

這也是過往我在 Vue 2 專案中的主要寫法。

// 父層組件
<template>
<div>
<ChildComponent :foo.sync="bar" />
</div>
</template>

<script>
export default {
data() {
return {
bar: '',
}
},
}
</script>
// 子層組件
<template>
<div>
<input :value="_foo" />
</div>
</template>

<script>
export default {
props: ['foo'],
computed() {
_foo {
get() {
return this.foo
},
set(newVal) {
this.$emit("update:foo", newVal)
},
},
},
}
</script>

這邊僅提出兩個較常見的寫法,其他還有很多寫法,但我們就不多贅述,趕緊把時間交棒給 Vue 3 吧。


Vue 3 時期

到了 Vue 3,從 Vue 2 遷移指南 當中官方很明確地告知我們使用 v-model 取代 .sync。除此之外,Vue 3 在 Composition API 也提供了 defineProps() 和 defineEmits() 這兩個巨集來取代 Options API 當中的 props 和 emit。那我們來看看 Vue 3 剛發佈時期我們怎麼做雙向綁定吧。


使用 defineProps() + defineEmits()

首先我們用官方提供的這兩個巨集來改寫一下先前的 .sync 寫法。

// 父層組件
<template>
<div>
<ChildComponent v-model:foo="bar" />
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
const bar = ref('')
</script>
// 子層組件
<template>
<div>
<input :value="_foo" />
</div>
</template>

<script lang="ts" setup>
const props = defineProps<{ foo: string }>()
const emit = defineEmits<{ 'update:foo': [id: string] }>()

const _foo = computed({
get() {
return props.foo
},
set(value) {
emit('update:foo', value)
},
})
</script>

眼尖的小夥伴有沒有發現,使用兩個單向資料流來組合父子層資料雙向綁定的整體概念與過去並沒有什麼差異,講白了在開發體驗上就是換湯不換藥。這時我們的 Vue 開發好麻吉-VueUse 聽到了你各位懶惰的心聲。


使用 VueUse 函式庫

在 VueUse 中提供了 useVModel 與 useVModels 兩個函式。兩者的差異,白話點講就是綁定一個或多個 v-model (或許未來有一天可以來聊聊 again)

父層組件的部分與前例一致,接下來我們就專注在子層組件上。

// 子層組件
// 使用 useVModel 綁定一個欄位
<template>
<input v-model="_foo" />
</template>

<script lang="ts" setup>
import { useVModel } from '@vueuse/core'

const props = defineProps<{ foo: string }>()
const emit = defineEmits(['update:foo'])

const _foo = useVModel(props, 'foo', emit)
</script>
// 子層組件
// 使用 useVModels 綁定多個欄位
<template>
<input v-model="data.foo" />
<input v-model="data.bar" />
</template>

<script lang="ts" setup>
import { useVModels } from '@vueuse/core'

const props = defineProps<{
foo: string
bar: string
}>()
const emit = defineEmits<{
'update:foo': [id: string]
'update:bar': [id: string]
}>()

const data = useVModels(props, emit)
</script>

好的,看起來我們只要把宣告好的 props 和 emit 交給 useVModel / useVModels,接著 VueUse 就會貼心地幫我們完成雙向綁定。文件上官方也講得很明,它其實就是 props + emit 並產出 ref 的語法糖。

Shorthand for v-model binding, props + emit -> ref

BUT!!!
不是欸,props 和 emit 我還是得自己寫呀!怎麼有種做事做一半的感覺,能不能拜託行行好,直接好人做到底啊。

此時時間來到 Vue 3.4 版本發佈,我們終於迎來了開發體驗的曙光。


使用 defineModel()

// 子層組件
<template>
<input v-model="foo" />
</template>

<script lang="ts" setup>
const foo = defineModel<string>('foo', { required: true })
</script>

OK,一行寫完。就是這麼簡單明瞭又暴力,堪稱工程師福音。現在就讓我們好好認識一下這個相見恨晚的巨集。(這麼多年了,你怎就不早點出現啊)

就是這麼簡單

就是這麼簡單


defineModel() 的相關用法

  1. 使用預設 v-model 名稱。
    在 Vue 3 中 v-model 預設 props 名稱為 modelValue,也就是說如果你不自訂名稱,直接使用 defineModel() 即可。
// 父層使用 v-model
const modelValue = defineModel() // 宣告即完成綁定 modelValue
modelValue.value = 'foo' // 修改即自動觸發 update:modelValue


  1. 自訂 v-model 名稱。
// 父層使用 v-model:foo​
const foo = defineModel('foo') // 宣告即完成綁定 foo
foo.value = 'bar' // 修改即自動觸發 update:foo


  1. 定義傳入參數的型別。
// 父層使用 v-model
const modelValue = defineModel({ type: String }) // JS
// or
const modelValue = defineModel<string>() // TS

// 父層使用 v-model:foo​
const foo = defineModel('foo', { type: String }) // JS
// or
const foo = defineModel<string>('foo') // TS


  1. 定義傳入參數的預設值。
// 父層使用 v-model
const modelValue = defineModel({ type: String, default: '我是預設值' }) // JS
// or
const modelValue = defineModel<string>({ default: '我是預設值' }) // TS​

// 父層使用 v-model:foo​
const foo = defineModel('foo', { type: String, default: '我是預設值' }) // JS
// or
const foo = defineModel<string>('foo', { default: '我是預設值' }) // TS


  1. 定義傳入參數是否為必填。
// 父層使用 v-model
const modelValue = defineModel({ type: String, required: true }) // JS
// or
const modelValue = defineModel<string>({ required: true }) // TS​

// 父層使用 v-model:foo​
const foo = defineModel('foo', { type: String, required: true }) // JS
// or
const foo = defineModel<string>('foo', { required: true }) // TS

這邊有個小技巧,當我們設定 required 時,該值的型別將自動排除掉 undefined。這在撰寫 TS 時,開發體驗真的大大加分。

const modelValue = defineModel<string>()
// modelValue 型別為 Ref<string | undefined>

const modelValue = defineModel<string>({ required: true })
// modelValue 型別為 Ref<string>


  1. 使用修飾符和轉換器

熟悉的修飾符也是可以用的。透過解構 defineModel 的回傳值來獲得 modelValue 和 modelModifiers,並且如過往操作 computed 一樣能夠使用 get 與 set,依照所需情境對資料進行加工處理。

// 父層使用 v-model.trim="foo"​
const [modelValue, modelModifiers] = defineModel<string>({
// get() 省略了,因為這裡不需要它
set(value) {
// 如果使用了 .trim 修飾符,則回傳剪裁過後的值
if (modelModifiers.trim) {
return value.trim()
}
// 否則,就直接回傳
return value;
},
})


結語

最後我想說的是,不管你是剛開始寫 Vue 的新手或是老鳥,從你認識 defineModel() 的那刻起,別再把生命浪費在瑣碎的事情上了。

一句話,真香。

寫過的都懂

寫過的都懂


Cheng's murmur

颱風假可以帶走你的上班日,
但帶不走你手頭上專案的 deadline 啊!
留言
avatar-img
留言分享你的想法!
avatar-img
Cheng's
2會員
6內容數
生活就是 早上 8 點的文湖線;晚上 8 點的 New York Sour;帶著一台 GR3X 意興闌珊的漫步;嚮往著午後草皮上陪拉布拉多 🐶 玩耍;拿起似有似無的筆開始敲打創作。
你可能也想看
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
VUE為單向資料流的框架,在鄰近層級之間我們可以依靠 props 由父層向子層來傳遞需要的資料,然而遇到跨層級的架構時,雖然也是可以一層層傳進去,只是這會造成多餘的處理及凌亂的程式碼,因此才有了 "provide" 來解決我們跨層級的需求。 層級展示圖
Thumbnail
VUE為單向資料流的框架,在鄰近層級之間我們可以依靠 props 由父層向子層來傳遞需要的資料,然而遇到跨層級的架構時,雖然也是可以一層層傳進去,只是這會造成多餘的處理及凌亂的程式碼,因此才有了 "provide" 來解決我們跨層級的需求。 層級展示圖
Thumbnail
Vue.js是一種基於MVVM的前端JavaScript框架,類似的框架有React、Angular等。 架設環境 安裝Visual Studio Code(https://code.visualstudio.com/) 安裝Node.js(https://nodejs.org/en/
Thumbnail
Vue.js是一種基於MVVM的前端JavaScript框架,類似的框架有React、Angular等。 架設環境 安裝Visual Studio Code(https://code.visualstudio.com/) 安裝Node.js(https://nodejs.org/en/
Thumbnail
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
Thumbnail
這系列是我在 2023 六角學院 Vue作品實戰班的筆記,筆記以本人理解的方式記錄。此篇主題為 Slot Props 進階應用 ,其中包含單筆資料、多筆資料。
Thumbnail
這系列是我在 2023 六角學院 Vue作品實戰班的筆記,筆記以本人理解的方式記錄。此篇主題為 Slot Props 進階應用 ,其中包含單筆資料、多筆資料。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News