开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情
前言
- 基础篇: 简单总结
Vue3
中v-model
的使用方式,包括在原生元素和组件上使用 - 进阶篇: 对
v-model
进行封装,编写一个选项式Api
, 方便在组件上使用 自定义v-model
基础
v-model
用于在表单处理中,使用 v-model
语法糖,可以很简单讲表单输入框中的内容雨 Javascript
中的变量进行同步
在单个元素上使用 v-model
<input v-model="value" />
v-model
除了在 input
上使用之外,还可用于 textarea
、 select
等元素,对于不同元素或者 input
不同 type
属性时, v-model
绑定的元素和监听的事件不同。来看下官网的描述
Vue
模板编译器会将 v-model
进行解析并展开,等价于下面的代码
<input :value="value" @input=value = $event.target.value />
v-model
可用的修饰符
v-model
修饰符可以将输入的只进行一定的处理之后在赋值给 Javascript
变量, v-model
在原生元素上支持一下修饰符。
.lazy 修饰符: 延迟到 change
事件之后更新数据
.number 修饰符: 自动将数据转化成 Number
类型
.trim 修饰符: 去除数据中两端的空格
组件上使用 v-model
上面回顾了一下在原生元素上使用 v-model 语法糖,在组件上使用 v-model 就没有像原生元素上那么简单,当子组件的数据发生改变,需要通过 emit 事件通知父组件更新数据。
在
Vue
中,父子组件中用于传递数据的props
数据为单向数据流,只能是父组件向子组件传递,子组件不直接修改props
的值,否则会抛出警告。
来看下组件中 v-model
的使用方式。
<!-- 子组件 -->
<template>
<input type="text" :value="modelValue" @input="update">
</template>
<script lang="ts" setup>
defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit: any = defineEmits();
const update = (e: any)=>{
emit('update:modelValue', e.target.value)
}
</script>
<!-- 父组件 -->
<BaseVModel v-model="val"></BaseVModel>
当在组件上使用 v-model
时,等价于以下代码:
<BaseVModel :modelValue="value" @update:modelValue="newVal => value = newVal"></BaseVModel>
默认情况下在组件中使用 v-model
, 子组件需要在 props
中定义一个 modelValue
值, 将 modelValue
绑定到 input
中 value
属性中作为初始值。 然后通过 emit
触发 update:modelValue
事件并传递新值,在这种情况下,不需要显示的注册 update:modelValue
事件, Vue
在编译过程中会自动注册。
v-model 参数
在组件上使用 v-model
时,如果你不想使用 modelValue
作为 props
变量名,那么可以使用 v-model
参数来自定义 props
变量名
<!-- 子组件 -->
<template>
<input type="text" :value="title" @input="update">
</template>
<script lang="ts" setup>
defineProps({
title: {
type: String,
default: ''
}
})
const emit: any = defineEmits();
const update = (e: any)=>{
emit('update:title', e.target.value)
}
</script>
<!-- 父组件 -->
<BaseVModel v-model:title="val"></BaseVModel>
父组件中使用 v-model:title
的形式来定义 v-model
的参数,其中的 title
与子组件中的 props
相对应,在子组件输入框中的数据发生变化时,则需要出发 update:title
事件来通知父组件更新对应的变量值。
通过 v-model
参数的方式,可以实现多个 v-model
绑定
<!-- 子组件 -->
<template>
<input type="text" :value="title" @input="updateTitle" />
<input type="text" :value="content" @input="updateContent" />
</template>
<script lang="ts" setup>
defineProps({
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
}
})
const emit: any = defineEmits();
const updateTitle = (e: any)=>{
emit('update:title', e.target.value)
}
const updateContent = (e: any)=>{
emit('update:content', e.target.value)
}
</script>
<!-- 父组件 -->
<BaseVModel v-model:title="val" v-model:content="content"></BaseVModel>
自定义修饰符
在原生元素上使用 v-model
只能使用内置的修饰符,在组件上使用 v-model
可以自定义修饰符,在自定义修饰符需要为每一个修饰符制定一个处理还是来修改抛出的值。
在子组件中,可以通过在 props
中声明 modelModifiers
来访问修饰符的值,如果存在修饰符,则 modelModifiers
对象中存在以修饰符 key
的属性,并且值为 true
。
对于有参数又有修饰符的情况下,子组件中对应的 props 名称将是 ${arg}
+ Modifiers
<!-- 子组件 -->
<template>
<input type="text" :value="title" @input="updateTitle" />
</template>
<script lang="ts" setup>
defineProps({
title: {
type: String,
default: ''
},
titleModifiers: { default: ()=> ({})}
})
const emit: any = defineEmits();
const updateTitle = (e: any)=>{
// 这里进行一些逻辑处理,然后出发事件
let value = e.target.value
if (props.titleModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:title', value)
}
</script>
<!-- 父组件 -->
<BaseVModel v-model:title.capitalize="val"></BaseVModel>
进阶
通过基础篇的一些总结,我们已经了解了如何使用 v-model
语法糖, 但是在开发过程中,这个过程略显繁琐,我们可以将子组件中一些逻辑进行抽离封装成 hook
,方便在实际开发中使用。
如何对 props 中的值进行绑定
对于这个问题, Vue
官方文档中给出了一种解决方式,就是使用一个可写,同时具有 setter
和 getter
的 computed
属性, get
方法返回 props
中对应的值, 而 set
方法则触发相应的事件。
<!-- 子组件 -->
<template>
<input type="text" v-model="titleValue" />
</template>
<script lang="ts" setup>
const props = defineProps({
title: {
type: String,
default: ''
},
modelModifiers: { default: ()=> ({})}
})
const emit: any = defineEmits();
const titleValue = computed({
get(){
return props.title
},
set(val){
emit('update:title', val)
}
})
</script>
还有另外一种方式,就是定义一个响应式变量,初始值设置为 props
中对应的值,然后使用 watch
监听这个响应式变量,当数据值发生变化时,出发相应的事件
封装 v-model 的 hook
基础功能
export function useVModel(options: ModelOptions){}
首先,我们定义一个函数 useVModel
,该函数接收一个配置对象, ModelOptions
定义如下
type ModelOptions = {
props: any // 这里为 vue props 类型,暂时用 any 代替
keys?: string[] // keys 为需要绑定的 props 中的 key 数组
emit: any // 事件触发器
}
接下来,我们将上面 props 数据绑定的逻辑进行抽离
export function useVModel(options: ModelOptions){
// 将配置对象进行解构取值
const {props, keys = ['modelValue'], emit} = options
// 遍历 keys ,将 props 中对应的属性转换成 computed 对象
let proxyObj = {}
keys.forEach((key)=>{
proxyObj[`${key}Proxy`] = computed({
get(){
return props[key]
},
set(val){
emit(`update:${key}`, val)
}
})
})
// 将 proxyObj 对象返回
return proxyObj
}
到这里, 我们完成一个基本的封装,下面来看下怎么使用
<!-- 子组件 -->
<template>
<input type="text" v-model="titleProxy" />
</template>
<script lang="ts" setup>
const props = defineProps({
title: {
type: String,
default: ''
},
})
const emit: any = defineEmits();
// 使用 useVModel 并结构返回值,然后在模版中使用 v-model 进行绑定即可
const { titleProxy } = useVModel({
props: props,
keys: ['title']
emit: emit
})
</script>
添加修饰符处理
在使用 v-model
时,可能会自定义修饰符,在封装 hooks
时,需要对自定义修饰符的逻辑进行处理。 我们对 useVModel
函数参数类型进行修改,增加一个可选参数 modifiers
, 该参数是一个对象,对象的 key
为 修饰符名称, 对应的值为修饰符的处理函数,处理函数接收表单数据作为参数,并需要将处理后的数据进行返回。
type ModelOptions = {
props: any
keys?: string[]
emit: any
modifiers?: { [key: string]: (val: any) => any }
}
接下来编写修饰符的处理逻辑
// 使用柯里化
function execModifiesProgress(props: any, modifiers: { [key: string]: <T>(value: T) => T }) {
return (key: string, val: any) => {
// 取得 props 属性对应的修饰符 名称
const modifierName = key === 'modelValue' ? `modelModifiers` : `${key}Modifiers`
const propsModifiers = props[modifierName]
// 如果该 props 没有定义修饰符,直接返回数据
if (!propsModifiers) {
return val
}
// 获取到该 props 所有的修饰符名称
const propsModifiersKeys = Object.keys(propsModifiers)
// 遍历所有的修饰符名称,取得相应的处理函数,并执行
propsModifiersKeys.forEach((modifierKey: string) => {
const callback = modifiers[modifierKey]
if (!callback) {
return
}
val = callback(val)
})
// 返回经过处理的最终值
return val
}
}
在修饰符进行处理过程中,需要考虑同一个 props
存在多个修饰符的情况;需要判断是否有定义修饰符, 对于 v-model
自定义的修饰符,需要在子组件中的 props
定义 ${arg}
+ Modifiers
名称的属性,默认为一个对象,否则, 无法处理该修饰符
最终完整 hooks 函数
import { ref, computed, getCurrentInstance } from 'vue'
type ModelOptions = {
props: any
keys?: string[]
emit: any
modifiers?: { [key: string]: (val: any) => any }
}
export function useVModel(options: ModelOptions) {
const { props, keys = ['modelValue'], emit, modifiers } = options
const proxyKeys = generateReturnValueType(keys)
type keysModule = typeof proxyKeys[number]
const _vm = getCurrentInstance()
const _emit = _vm?.emit
let prxoyObj: { [key in keysModule]: any } = {}
const modifierProgress = modifiers ? execModifiesProgress(props, modifiers) : (key: string, val: any) => val
keys.forEach((key: string) => {
const name = `${key}Proxy`
prxoyObj[name] = computed({
get() {
return props[key]
},
set(val) {
_emit!(`update:${key}`, modifierProgress(key, val))
}
})
})
return prxoyObj as { [key in keysModule]: any }
}
function generateReturnValueType(keys: string[]): string[] {
return keys.map((key: string) => {
return `${key}Proxy`
})
}
function execModifiesProgress(props: any, modifiers: { [key: string]: <T>(value: T) => T }) {
return (key: string, val: any) => {
const modifierName = key === 'modelValue' ? `modelModifiers` : `${key}Modifiers`
const propsModifiers = props[modifierName]
if (!propsModifiers) {
return val
}
const propsModifiersKeys = Object.keys(propsModifiers)
console.log(propsModifiersKeys)
// 考虑存在多个修饰符的情况
propsModifiersKeys.forEach((modifierKey: string) => {
const callback = modifiers[modifierKey]
if (!callback) {
return
}
val = callback(val)
})
return val
}
}
使用实例
<!-- 子组件 -->
<script setup lang="ts">
import { useVModel } from '../utils/useVModel'
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
content: String,
contentModifiers: { default: ()=> ({}) }
})
const emit = defineEmits()
const { modelValueProxy, contentProxy } = useVModel({
props: props,
emit: emit,
keys: ['modelValue', 'content'],
modifiers: {
aaa: (val): string => {
return '1' + val
},
bbb: (val): string=>{
return '0' + val
},
ccc:(val): string=>{
return '2' + val
},
ddd: (val): string=>{
return 'a' + val
}
}
})
</script>
<template>
<input type="text" v-model="modelValueProxy" />
<input type="text" v-model="contentProxy" />
</template>
<!-- 父组件 -->
<template>
<HelloWorld v-model.aaa.bbb.ccc="title" v-model:content.ddd = "content" />
</template>
总结
在封装 hooks
,需要考虑多种情况,例如: props
中哪些是需要进行 v-model
绑定; 需要处理多个修饰符的情况。
上述代码只是一个示例,还存在许多不足和可优化的地方,欢迎各位大佬批评指正。