Vue 3 双向数据绑定

Flying
2021-09-25 / 0 评论 / 240 阅读 / 正在检测是否收录...

双向数据绑定在前端最早用在 KO、AngularJS 这些 MVVM 框架中,它是 Vue 从诞生就一直有的特性。使用 v-model 可以在组件上实现双向绑定,方便表单应用的开发。相比 Vue 2,Vue 3 的双向绑定功能得到了增强,具体表现在以下几个方面:

  • 支持 v-model 的参数
  • 支持多个 v-model 绑定
  • 支持自定义 v-model 修饰符

two-way-binding-vue.svg

v-model 的参数

在 Vue 2 中,默认情况下,v-model 在组件上都是使用 value 作为 prop,并以 input 作为对应的事件。对于单选框、复选框等类型的单个输入控件,可以通过设置 model 选项的 prop 为 checked, event 为 change。无论设不设置 model 选项,这些名称都是固定不变的。

在 Vue 3 中,默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。

<!-- CustomInput.vue --> <script setup lang="ts"> defineProps<{ modelValue?: string }>() const emit = defineEmits<{ (e: 'update:modelValue', vlaue: string): void }>() </script> <template> <input :value="modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" /> </template>

这样我们就自定义了一个支持 v-model 的 Vue 3 组件。在宿主组件中使用时与 Vue 2 中相同:

<!-- App.vue --> <script lang="ts" setup> import { ref } from 'vue' import CustomInput from './components/CustomInput.vue' const message = ref('hello') </script> <template> <CustomInput `v-model`="message" /> <p>{{ message }}</p> </template>

不同的是,在 Vue 3 中,prop 和事件的名称时可变的:

<script setup lang="ts"> defineProps<{ title?: string }>() const emit = defineEmits<{ (e: 'update:title', vlaue: string): void }>() </script> <template> <input :value="modelValue" @input="emit('update:title', ($event.target as HTMLInputElement).value)" /> </template>

上述示例中,我们为自定义组件设置了 title prop 和 update:title 事件。因此,使用时,我们要给 v-model 指定一个参数来更改这些名称,如下面示例中的参数 title:

<template> <CustomInput `v-model`:title="message" /> <p>{{ message }}</p> </template>
默认的 v-model 参数为 modelValue,为了简便,一般在使用是会省去该参数。

注意:双向绑定 prop 和事件名也不是任意的,规则是:事件名 = update: + prop 名,这和为组件 prop 自定义 .sync 修饰符时需要遵从的模式类似。

v-model 参数指定参数与事件名不仅表达了绑定的意图,更重要的意义在于,它使绑定多个 v-model成为可能。

绑定多个 v-model

在 Vue 2 中,只能绑定一个 v-model,但可以通过 .sync 修饰符“双向绑定”多个 prop。Vue 3 借鉴并简化了 .sync 修饰符模式,直接支持多个 v-model 绑定。

// ... import { computed } from 'vue' const props = withDefaults( defineProps<{ modelValue?: string percent?: number modelModifiers?: { capitalize: boolean } }>(), { modelValue: '', percent: 0 } ) const emit = defineEmits<{ (e: 'update:modelValue', value: string): void (e: 'update:percent', value: number): void }>() const changeValue = (event: Event) => { let value = (event.target as HTMLInputElement).value emit('update:modelValue', value) } const current = computed({ get() { return Math.round(props.percent * 100) }, set(value: number) { emit('update:percent', value / 100) } })

上述示例中,我们自定义了两个 v-model: modelValuepercent

如你所见,上述示例在组件内实现 v-model 的方式是使用一个可写的 computed 属性。get 方法需返回 percent prop,而 set 方法需触发相应的 update:percent 事件。

组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:

<Form v-model="name" v-model:percent.number="percent" />

有了多个 v-model 绑定,可以不用再为表单中的每个字段都自定义一个支持 v-model 的组件,现在可以只自定义一个表单组件,每个字段绑定一个 v-model

自定义 v-model 修饰符

在 Vue 2 中,我们可以在 v-model 上使用一些内置的修饰符,例如 .trim.number.lazy。在 Vue 3 中,我们可以继续使用这些修饰符,而且还可以自定义修饰符。帅吧✌️

  1. 首先声明 modelModifiers prop,它的默认值是一个空对象:
modelModifiers?: { capitalize: boolean }
注意这里组件的 modelModifiers prop 包含了 capitalize 且其值为 true,它在模板中的 v-model 绑定时会用到。
  1. 可以检查 modelModifiers 对象的键,并编写一个处理函数:每次 <input> 元素触发 input 事件时将值的首字母大写:

    if (props.modelModifiers?.capitalize) { value = value.charAt(0).toUpperCase() + value.slice(1) }
  2. 使用时直接在 v-model 后面加 .capitalize
<Form v-model.capitalize="name" v-model:percent.number="percent" />
对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名将是:参数名 + 修饰符名。

测试 v-model

基本套路是通过 props 渲染选项传入 prop 初始值,然后断言这些初始值是否正确。接下来更改组件值,断言是否派发了相应 update 事件:

import { render, fireEvent } from '@testing-library/vue' import Form from './Form.vue' test('v-model', async () => { const { emitted, getByRole, rerender } = render(Form, { props: { modelValue: '', modelModifiers: { capitalize: true } } }) const nameInput = getByRole('textbox') expect(nameInput).toHaveValue('') await fireEvent.update(nameInput, 'something') expect(emitted()['update:modelValue'][0]).toEqual(['Something']) await rerender({ percent: 0 }) const percentInput = getByRole('spinbutton') expect(percentInput).toHaveValue(0) await fireEvent.update(percentInput, '24') expect(emitted()['update:percent'][0]).toEqual([0.24]) })
Vue Testing Library v6.1 新增了 rerender 渲染器方法, 类似 Vue Test Utils 中的 setProps 包裹器方法,我们可以在单个测试用例中动态更改 props 渲染选项。

参考链接

1

评论 (0)