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
2會員
6內容數
生活就是 早上 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
大家好,我是woody,是一名料理創作者,非常努力地在嘗試將複雜的料理簡單化,讓大家也可以體驗到料理的樂趣而我也非常享受料理的過程,今天想跟大家聊聊,除了料理本身,料理創作背後的成本。
Thumbnail
哈囉~很久沒跟各位自我介紹一下了~ 大家好~我是爺恩 我是一名圖文插畫家,有追蹤我一段時間的應該有發現爺恩這個品牌經營了好像.....快五年了(汗)時間過得真快!隨著時間過去,創作這件事好像變得更忙碌了,也很開心跟很多厲害的創作者以及廠商互相合作幫忙,還有最重要的是大家的支持與陪伴🥹。  
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
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
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
一般而言,組件之間的資料傳遞,可以使用 props 來達成,不過一旦層級過多的時候,props 就要逐層向下傳遞,會越來越麻煩且複雜。 而 provide、inject 可以解決這個問題,它可以提供一個「源頭」,子組件們可以藉由同一個源頭取得對應的資料,且沒有層級分別,都可以取得,就不用逐層傳遞資
當 父組件 有數據想傳送到 子組件 就可以使用props 1​. 父層傳遞設置 可以在父組件的屬性給予一個值,當作要傳送到子組件的資料。 父層組件​ : <!-- App.vue (父組件) --> <template> <div> <ChildComponent greetin
Thumbnail
這系列是我在 2023 六角學院 Vue作品實戰班的筆記,筆記以本人理解的方式記錄。此篇主題為 Slot Props 進階應用 ,其中包含單筆資料、多筆資料。
Thumbnail
在 Vue 中,組件是構建應用程式的基本單位,而 props 是組件間傳遞資料的主要方式之一,本文將介紹 Vue 中的 props,並通過實際範例展示如何使用 props 實現組件間的資料傳遞。
Thumbnail
v-model 指令是 Vue 中一個非常重要的功能,它實現了表單輸入和應用狀態之間的雙向綁定。本文將介紹如何使用 v-model 指令。
Thumbnail
大家好,我是woody,是一名料理創作者,非常努力地在嘗試將複雜的料理簡單化,讓大家也可以體驗到料理的樂趣而我也非常享受料理的過程,今天想跟大家聊聊,除了料理本身,料理創作背後的成本。
Thumbnail
哈囉~很久沒跟各位自我介紹一下了~ 大家好~我是爺恩 我是一名圖文插畫家,有追蹤我一段時間的應該有發現爺恩這個品牌經營了好像.....快五年了(汗)時間過得真快!隨著時間過去,創作這件事好像變得更忙碌了,也很開心跟很多厲害的創作者以及廠商互相合作幫忙,還有最重要的是大家的支持與陪伴🥹。  
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
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
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
一般而言,組件之間的資料傳遞,可以使用 props 來達成,不過一旦層級過多的時候,props 就要逐層向下傳遞,會越來越麻煩且複雜。 而 provide、inject 可以解決這個問題,它可以提供一個「源頭」,子組件們可以藉由同一個源頭取得對應的資料,且沒有層級分別,都可以取得,就不用逐層傳遞資
當 父組件 有數據想傳送到 子組件 就可以使用props 1​. 父層傳遞設置 可以在父組件的屬性給予一個值,當作要傳送到子組件的資料。 父層組件​ : <!-- App.vue (父組件) --> <template> <div> <ChildComponent greetin
Thumbnail
這系列是我在 2023 六角學院 Vue作品實戰班的筆記,筆記以本人理解的方式記錄。此篇主題為 Slot Props 進階應用 ,其中包含單筆資料、多筆資料。
Thumbnail
在 Vue 中,組件是構建應用程式的基本單位,而 props 是組件間傳遞資料的主要方式之一,本文將介紹 Vue 中的 props,並通過實際範例展示如何使用 props 實現組件間的資料傳遞。
Thumbnail
v-model 指令是 Vue 中一個非常重要的功能,它實現了表單輸入和應用狀態之間的雙向綁定。本文將介紹如何使用 v-model 指令。