理解状态的位置和组件边界仍然是现代前端开发中主要挑战之一,也是团队在应用规模增加时做出的最重要的决定之一,可能会加速开发,也可能成为最大的摩擦源。
如果做得好,构建、组合、重构和测试前端组件会变得轻而易举;如果做得不好,则会成为难以追踪的幽灵错误的无尽源泉,使代码库变得脆弱。
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.28at.com/showinfo-26-96994-0.htmlVue 3.4 重磅升级:defineModel 宏如何彻底改变前端状态管理!
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com