理解狀態的位置和組件邊界仍然是現代前端開發中主要挑戰之一,也是團隊在應用規模增加時做出的最重要的決定之一,可能會加速開發,也可能成為最大的摩擦源。
如果做得好,構建、組合、重構和測試前端組件會變得輕而易舉;如果做得不好,則會成為難以追蹤的幽靈錯誤的無盡源泉,使代碼庫變得脆弱。
Vue 3.4 版本中從實驗狀態發布的 defineModel 宏,可能是關于組件間復雜狀態交互的實現方式最具變革性的特性之一。
描述看似很無害:
defineModel 是一個新的 <script setup> 宏,旨在簡化支持 v-model 的組件的實現
表面上看,這個宏的實用性似乎微不足道,但它對團隊如何處理狀態和管理組件邊界有深遠影響。我們看看 defineModel 是做什么的,為什么它的添加在 Vue 3.4 中感覺像是一種范式轉變——盡管它只是一個簡單的宏。
通常,現代前端應用程序有三種狀態范圍(不包括全局窗口級別的狀態)。
圖片
在全局級別,有許多庫和解決方案可以解決這個問題。例如,React 的 Zustand、Jotai、Recoil、Redux 等,Vue 的 Pinia 可以將狀態從組件樹中提取出來放入全局范圍,以跨越樹。它旨在保存真正的全局狀態,如淺色/深色模式或租戶 ID。
第二層狀態是團隊遇到“屬性傳遞”摩擦的地方——無論是在 React、Vue 還是其他庫或框架中。部分原因是管理狀態在組件之間的上下移動是繁重的。
在這種情況下,團隊的自然決定是將狀態移動到全局存儲中,或者進入第三個組件狀態范圍,僅僅是為了避免這種摩擦,而不斷堆積到一個巨大的組件中——這會產生另一種痛苦。
如果能在保持 Vue 的雙向綁定的同時,輕松地將狀態分離開來,而不需要屬性傳遞的摩擦,那該多好。這正是 defineModel 的作用所在,它大大減少了在樹中移動狀態的摩擦,同時保持 Vue 的雙向綁定。
重要的是首先了解它是什么以及它的功能。對于那些不熟悉 Vue 的人來說,組件間上下移動狀態的慣用模式一直是使用 props 和 emits。
例如,考慮這個父子組件:
圖片
外部組件定義了 ref 并將其作為 prop 傳遞給子組件。更新通過子組件向父組件的 emit 事件來完成。
為了獲得雙向綁定,我們需要內部的 NameInput.vue 組件如下:
<!-- NameInput.vue --><template> <LabeledContainer label="NameInput.vue"> <input v-model="name"/> </LabeledContainer></template><script setup lang="ts">const props = defineProps<{ modelValue: string}>()const emits = defineEmits<{ 'update:modelValue': [string]}>()const name = computed({ get() { return props.modelValue }, set(val) { emits('update:modelValue', val) }})</script>
外部的 Example1.vue 組件如下:
<!-- Example1.vue --><template> <LabeledContainer label="Example1.vue"> <h1>Example 1</h1> <p>Hello, {{ name.length === 0 ? "(enter your name below)" : name }}</p> <NameInput v-model="name"/> </LabeledContainer></template><script setup lang="ts">const name = ref('')</script>
現在,當我們在文本框中輸入值時,它會自動更新 prop 的值:
圖片
我們的非常簡單的組件具有父子組件之間的雙向綁定。
可以很容易地看到,對于如此簡單的事情,這種樣板代碼會變得多么繁瑣!
隨著 Vue 3.4 中 defineModel 的發布,我們看看它如何簡化 NameInput.vue:
<!-- NameInput.vue --><template> <LabeledContainer label="NameInput.vue"> <input v-model="name"/> </LabeledContainer></template><script setup lang="ts">const name = defineModel<string>({ required: true })</script>
父組件保持不變,但大量的樣板代碼被刪除了!這個小小的宏完全改變了管理狀態的體驗。
表面上看,這似乎是一個微不足道的變化。當然,獲得了一些便利,但這對開發人員管理狀態有多大影響?僅僅是一個簡單的宏,聲稱會有這么大的影響豈不是荒謬?
事實上,開發人員往往會選擇阻力最小的路徑,如果阻力最小的路徑是壞習慣,那么,開發人員將創建一個充滿許多壞習慣的代碼庫——即“技術債務”。如果你見過 1000 多行的 React 或 Vue 組件(我們中誰沒見過?),那么很可能的原因是將狀態以可管理的方式分散出來的摩擦太大;隨著組件的有機增長,將狀態保持在一個巨大的組件中比分解出新組件更容易。
defineModel 的實現是,它創建了一條最小阻力路徑,同時有助于改善團隊對狀態的思考方式。突然之間,管理分層組件狀態變得微不足道,并消除了將狀態移入全局范圍或在大型組件中進行松散操作的誘惑(通常是 1000 多行組件的來源)。
考慮以下簡單的聯系人管理應用程序:
圖片
注意這個示例中的層次結構。當用戶從 Listing.vue 中選擇聯系人時,應用程序應在 Details.vue 中顯示詳細信息。當用戶編輯詳細信息并在 Details.vue 中保存更改時,應用程序應更新 Listing.vue 中的條目。
如果我們想在 Listing.vue 和 Details.vue 之間共享狀態,它必須是全局狀態或從公共父級 Example3.vue 開始的分層狀態——否則,很容易看到將所有內容放入一個巨大的組件中的誘惑!
在這種情況下,這就是我們的分層狀態的樣子:
圖片
狀態通過 prop 從列表組件傳遞到聯系人組件。
我們從外到內檢查代碼。
這是我們的父 Example3.vue 組件:
<template> <LabeledContainer label="Example3.vue"> <h1>Example 3</h1> <p v-if="!!selectedContact"> Selected: {{ selectedContact.name }} ({{ selectedContact.handle }}) </p> <div class="parent"> <Listing v-model="contacts" v-model:selected="selectedContact"/> <Details v-model="selectedContact"/> </div> </LabeledContainer></template><script setup lang="ts">const selectedContact = ref<Contact>()const contacts = ref<Contact[]>([{ name: 'Charles', handle: '@chrlschn'}])</script>
這是我們的狀態所在根,并通過綁定將其傳遞給 Listing 和 Details 組件:
<!-- Snippet from Example3.vue--><Listing v-model="contacts" v-model:selected="selectedContact"/><Details v-model="selectedContact"/>
我們先看看 Details.vue :
<!-- Details.vue, the right side form inputs --><template> <LabeledContainer label="Details.vue"> <div v-if="!!selected"> <label> Name <input v-model="name"/> </label> <label> Handle <input v-model="handle"/> </label> <div> <button @click="handleCancel">Done</button> <button @click="handleDone">Save</button> </div> </div> <p v-else> Select a contact </p> </LabeledContainer></template><script setup lang="ts">const selected = defineModel<Contact|undefined>({ required: true})const name = ref('')const handle = ref('')watch (selected, (contact) => { if (!contact) { return } name.value = contact.name, handle.value = contact.handle})function handleCancel() { selected.value = undefined}function handleDone() { if (!selected.value) { return } selected.value.name = name.value; selected.value.handle = handle.value;}</script>
這個組件的目的是擁有一組狀態副本,當選中的聯系人更改時,組件將值復制到本地狀態,以便在不影響原始狀態的情況下(直到用戶保存),更改名稱和 handle。這也允許用戶取消任何編輯。
對于更大的屬性集,可以考慮創建對象的完整響應式副本并直接綁定到它。
在左側,Listing.vue 組件包含聯系人列表,并有添加新聯系人的選項。
<!-- Listing.vue --><template> <LabeledContainer label="Listing.vue"> <div class="container"> <ContactItem v-for="contact in contacts" :cnotallow="contact" :selected="selected == contact" @click="selected = contact"> </ContactItem> </div> <div> <button @click="handleAddContact"> Add contact </button> </div> </LabeledContainer></template><script setup lang="ts">const contacts = defineModel<Contact[]>({ required: true})const selected = defineModel<Contact|undefined>('selected', { required: true})function handleAddContact() { contacts.value.push({ name: 'Name', handle: 'Handle' })}</script>
然后在 ContactItem.vue 中,Listing.vue 通過普通的 props 傳遞顯示值,因為這里不需要變更(也不需要雙向綁定):
<template> <LabeledContainer label="Contact.vue" class="contact" :class="{ 'selected': !!selected }"> <p class="name">{{ contact.name }}</p> <p class="handle">{{ contact.handle }}</p> </LabeledContainer></template><script setup lang="ts">defineProps<{ contact: Contact, selected?: boolean}>()</script>
我們看看這整個事情是如何結合在一起的:
圖片
我們的組件在示例組件樹的層次結構中共享狀態。
如果沒有 defineModel 來幫助簡化這種交互,很容易看到本能是采取捷徑或將狀態移入全局狀態,因為編寫各種 emits 和 computed 會產生相當大的摩擦,即使在這個小示例中也是如此!
正如 Billy Mays 可能會說:“但等一下!還有更多!”讓我們看看如何通過使用可組合組件進一步簡化代碼。
利用可組合組件可以將其提升到一個新的水平,并通過將狀態從組件中提取出來進一步簡化我們的代碼。當組件變得更大時,這特別有用。
在 Vue 中,這很容易實現,并使重構和重新組織復雜性變得輕而易舉。
我們只需將我們的狀態和函數向上提取到另一個函數中:
// useContacts composableexport function useContacts() { const selectedContact = ref<Contact>() const contacts = ref<Contact[]>([{ name: 'Charles', handle: '@chrlschn' }]) function addContact() { contacts.value.push({ name: 'Name', handle: 'Handle' }) } return { selectedContact, contacts, addContact }}
很容易看出,如果我們想從 Details.vue 中移動更多的邏輯和狀態,將 name 和 handle refs 以及 handleCancel() 和 handleDone() 函數移動到另一個可組合組件中并共享它們是非常低摩擦的:
// useDetailsEditor.ts//
本文鏈接:http://www.tebozhan.com/showinfo-26-96994-0.htmlVue 3.4 重磅升級:defineModel 宏如何徹底改變前端狀態管理!
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com