Vue 3 双向数据绑定

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

双向数据绑定在前端最早用在 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)

取消