通过封装一个自定义 UI 组件来学习 v-model 原理

三葉Leaves Author

v-model 是 vue 的响应式体系里很方便的一个指令,了解其源码可以加深对各种 UI 组件库的理解,在某些企业的面试中也会发挥作用。

用于 HTML 标签的 v-model

下面两种方式完全等价

1
<input type="text" v-model="username">  
1
<input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"/>

方式 2 就是 v-model 的底层原理了。

  • :value="username" 是 JS 到页面的绑定
  • @input=" xxx " 是页面输入到 JS 变量值的绑定

其中的 $event 其实就是原生 DOM 事件对象,由于事件可能为空,所以加了一个 TS 的断言。

不过这只是一个前置知识,为了引出下面:

用于自定义组件的 v-model

这是 v-model 源码的一个实际应用,常用于封装自定义组件的 UI 组件库。

首先需要知道,对于自定义组件,下面两种方式仍然是等价的:

1
<LInput v-model="username"/>  
1
2
3
4
<LInput :modelValue="username"  
@update:modelValue="username = $event"
/>
<!-- update:modelValue 其实就是自定义事件的事件名 -->
为什么是 $event 而不是 $event.target.value 或者其他乱七八糟的东西?

因为:对于自定义事件,$event 是触发该事件的时候所传递的数据

不过到这里,v-model 并不会发挥作用。我们需要先了解组件通信的一些原理:

  • 在父传子上,我们使用 props 方式;
  • 在子传父上,我们使用绑定自定义事件的方式。

这时候就有思路了,显然我们需要写一个自定义组件来实现这两种通信,例如 src/components/LInput.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup lang="ts">  
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

</script>

<template>
<!-- 在每次 input 的时候触发事件,并且传递输入框的 value 属性值 -->
<input type="text"
:value="modelValue"
@input="emit('update:modelValue',(<HTMLInputElement>$event.target).value)"
/>
</template>

<style scoped>
input {
border: 2px dashed #ccc;
}

</style>

到这里,我们实际上自己封装了一个自定义组件,该组件同样具有原生 HTML 元素的 v-model 功能。

自定义 modelValue 名来定义多个 v-model

实际上,一个自定义组件可以绑定多个 modelValue:

1
<LInput v-model:name="username" v-model:password="password" />

v-model:name 这种写法实际上就是把源码写法中的 modelValue 给重命名了。值得注意的是,自定义事件名会变成:@update:name=" xxx " ,前面的 update 是固定的。

然后调整自定义组件的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">  
defineProps(['name', 'password'])
const emit = defineEmits(['update:name', 'update:password'])

</script>

<template>
<input type="text"
:value="name"
@input="emit('update:name',(<HTMLInputElement>$event.target).value)"
/>
<input type="text"
:value="password"
@input="emit('update:password',(<HTMLInputElement>$event.target).value)"
/>
</template>

<style scoped>
input {
border: 2px dashed #ccc;
}

</style>

可以看到,现在在自定义组件内,我们同时使用了两个 input,这两个都可以实现数据的双向绑定。

想学的更深?

本文中其他一些可以优化的点:

使用 computed 简化双向绑定

  • [ ] 在自定义组件内,使用 computed 简化双向绑定逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{
modelValue: string
}>()

const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()

// 创建一个可写的计算属性
const value = computed({
// getter:读取父组件传来的值
get() {
return props.modelValue
},
// setter:当内部 <input> 更新时,通知父组件
set(newValue) {
emit('update:modelValue', newValue)
}
})
</script>

<template>
<!-- 现在可以直接在内部 input 上使用 v-model 了! -->
<input type="text" v-model="value" />
</template>

<style scoped>
input {
border: 2px dashed #ccc;
}
</style>

v-model 的多样性

  • [ ] v-model 在原生元素上的多样性:

  • checkbox / radio:编译为 :checked 和 @change 事件。

  • select:编译为 :value 和 @change 事件。

  • 有 .lazy 修饰符的输入框:v-model.lazy=“msg” 会使用 @change 而不是 @input 事件。

v-model 修饰符

  • [ ]  v-model 修饰符

自定义组件也可以支持修饰符,例如 v-model.capitalize=“myText”。

当父组件这样使用时,子组件会通过一个特殊的 prop modelModifiers 来接收修饰符。

TypeScript 类型增强

自定义组件内的声明部分可以这样写:

1
2
3
4
5
6
7
8
9
const props = defineProps<{
name: string
password: string
}>()

const emit = defineEmits<{
(e: 'update:name', value: string): void
(e: 'update:password', value: string): void
}>()
  • 标题: 通过封装一个自定义 UI 组件来学习 v-model 原理
  • 作者: 三葉Leaves
  • 创建于 : 2025-06-30 00:00:00
  • 更新于 : 2025-07-10 13:40:50
  • 链接: https://blog.oksanye.com/a19819de7a9b/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论