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

更新於 2024/09/26閱讀時間約 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
2會員
5內容數
生活就是 早上 8 點的文湖線;晚上 8 點的 New York Sour;帶著一台 GR3X 意興闌珊的漫步;嚮往著午後草皮上陪拉布拉多 🐶 玩耍;拿起似有似無的筆開始敲打創作。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Cheng's 的其他內容
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
「在 JavaScript 中 0.1 + 02 等於多少?」 這是我在面試時會問的一題。有經驗的工程師應該知道我在問什麼,但相信仍有不少人可能還不知道 0.1 + 0.2 不等於 0.3。
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
「在 JavaScript 中 0.1 + 02 等於多少?」 這是我在面試時會問的一題。有經驗的工程師應該知道我在問什麼,但相信仍有不少人可能還不知道 0.1 + 0.2 不等於 0.3。
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
在 Vue 專案中使用 Apollo Graphql Client 從 API 獲取資料,由於資料結構較為複雜,筆者便跟著網路教學使用 codegen 工具自動化產生 TypeScript 型別定義。在某個元件中,需要使用 defineProps 來撰寫型別定義,結果⋯⋯
Thumbnail
這篇文章記錄了如何在網頁中使用<iframe>或套件vue3-google-map來實現Google Map呈現,並在上面設置圖釘。兩種方法的優缺點也有相關的說明。想要在網頁上加入Google Map及設置圖釘的讀者可以參考這篇文章。
父元件 傳遞方法使用@ <template>    ...    <Login @modalClose="modalClose"/> ... </template> <script setup>     const _modal = ref();     function m
父元件 傳遞變數時須加上冒號 子元件 接收props用法如下 本筆記參考: 1. https://www.netlify.com/blog/understanding-defineprops-and-defineemits-in-vue-3.2 2. https://juejin.cn/post/7
Thumbnail
雖然距離上次Vue直播班課不到一年,但看到這次的課程有Pinia內容,手又不小心刷了魔法小卡(? 意識到自己的成長應該是可以輕鬆地串接API,畢竟去年也已經串到可以去烤串店了XD,就算是重新複習,還是可以從中獲得新的成長,而且發現到老師一年講的比一年還好,今年有很多觀念講得更清楚了! 可惜這次第六週
Thumbnail
專案建好了,那先來講 Vue 的專案架構 詳細內容很多,所以我挑重點講 public index.html public/index.html 是 Vue 頁面的 entry point,進入一個 Vue 頁面會先進 public/index.html,再套用 App.vue,最後才是進入你寫的 .
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
在 Vue 專案中使用 Apollo Graphql Client 從 API 獲取資料,由於資料結構較為複雜,筆者便跟著網路教學使用 codegen 工具自動化產生 TypeScript 型別定義。在某個元件中,需要使用 defineProps 來撰寫型別定義,結果⋯⋯
Thumbnail
這篇文章記錄了如何在網頁中使用<iframe>或套件vue3-google-map來實現Google Map呈現,並在上面設置圖釘。兩種方法的優缺點也有相關的說明。想要在網頁上加入Google Map及設置圖釘的讀者可以參考這篇文章。
父元件 傳遞方法使用@ <template>    ...    <Login @modalClose="modalClose"/> ... </template> <script setup>     const _modal = ref();     function m
父元件 傳遞變數時須加上冒號 子元件 接收props用法如下 本筆記參考: 1. https://www.netlify.com/blog/understanding-defineprops-and-defineemits-in-vue-3.2 2. https://juejin.cn/post/7
Thumbnail
雖然距離上次Vue直播班課不到一年,但看到這次的課程有Pinia內容,手又不小心刷了魔法小卡(? 意識到自己的成長應該是可以輕鬆地串接API,畢竟去年也已經串到可以去烤串店了XD,就算是重新複習,還是可以從中獲得新的成長,而且發現到老師一年講的比一年還好,今年有很多觀念講得更清楚了! 可惜這次第六週
Thumbnail
專案建好了,那先來講 Vue 的專案架構 詳細內容很多,所以我挑重點講 public index.html public/index.html 是 Vue 頁面的 entry point,進入一個 Vue 頁面會先進 public/index.html,再套用 App.vue,最後才是進入你寫的 .