v-model

v-model支持的元素

v-model其实就是一个语法糖,通过props和emits组合而成的。

Vue3的v-model相比Vue2作了如下变动:

  1. prop:value→modelValue
  2. 事件:input→update:modelValue(Vue2里sync的写法)
  3. 移除了v-bind的.sync修饰符和组件的model选项
  4. 新增了支持多个v-model的特性
  5. 新增支持自定义修饰符Modifiers的特性

App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h1>App.vue(父组件)</h1>
<div>isShow:{{ isShow }}</div>
<div>text:{{ text }}</div>
<div><button @click="isShow = !isShow">开关</button></div>
<hr>
<!-- Vue3支持多个v-model,通过冒号后面的名字进行接收 -->
<!-- Vue3还支持自定义修饰符,这里加入了自定义修饰符abc -->
<VModel v-model="isShow" v-model:textVal.abc="text"></VModel>
</div>
</template>

<script setup lang="ts">
import VModel from "@/components/VModel.vue"

const isShow = ref<boolean>(false)
const text = ref<string>("yajue")
</script>

VModel.vue:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<div v-if="modelValue" class="model">
<div class="close"><button @click="close">关闭</button></div>
<h3>我是v-model子组件</h3>
<div>内容:<input @input="change" :value="textVal" type="text"></div>
</div>
</template>

<script setup lang="ts">

// vue3 默认值modelValue
const props = defineProps<{
// 收到了父组件传来的isShow
modelValue: boolean
// 收到了父组件传来的text
textVal: string
// 如果是默认值的话就是modelModifiers
textValModifiers?: {
// 都是布尔值
abc:boolean
}
}>()

// vue3 默认值update:modelValue
const emit = defineEmits(["update:modelValue","update:textVal"])

const close = ()=> {
// 给父组件回传isShow
emit("update:modelValue",false)
}

const change = (e:Event)=> {
const target = e.target as HTMLInputElement
// 给父组件回传text
// 如果加入了abc修饰符,每次输入时在text的末尾加上abc,否则不加
emit("update:textVal", props?.textValModifiers?.abc? target.value + "abc": target.value)
}

// 很多第三方组件上的v-model就是这么封装的
</script>

<style scoped>
.model {
width: 500px;
border: 5px solid #ccc;
padding: 10px;
}
</style>

源码

在Vue源码(/package/runtime-dom/src/directives/vModel.ts)中可以看到v-model的源码。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// We are exporting the v-model runtime directly as vnode hooks so that it can
// be tree-shaken in case v-model is never used.
// 一个自定义指令
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
// el dom节点对象 binding对象(内置修饰符) vnode
created(el, { modifiers: { lazy, trim, number } }, vnode) {
// 获取props中的modelValue属性对应的函数
el._assign = getModelAssigner(vnode)
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
// 通过addEventListener 如果有lazy修饰符,触发change
// 因为change只有在改变值且鼠标离开焦点才会触发,而input会频繁触发
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
// 如果有trim修饰符,去除左右空格
if (trim) {
domValue = domValue.trim()
}
// 如果有number修饰符,转成数字
if (castToNumber) {
domValue = looseToNumber(domValue)
}
el._assign(domValue)
})
// 如果触发的是change事件,有修饰符trim也要去除左右空格
if (trim) {
addEventListener(el, 'change', () => {
el.value = el.value.trim()
})
}
if (!lazy) {
// lazy 中文输入法特殊处理
// 当用户选中输入法输入的值,手动触发input
addEventListener(el, 'compositionstart', onCompositionStart)
addEventListener(el, 'compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
addEventListener(el, 'change', onCompositionEnd)
}
},
// set value on mounted so it's after min/max for type="range"
mounted(el, { value }) {
// 初始化赋值,绑定的值赋给value,目前只是单向流动
el.value = value == null ? '' : value
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
if (document.activeElement === el && el.type !== 'range') {
// 如果设了lazy,返回
if (lazy) {
return
}
// 如果设了trim且值已经去过空格,返回
if (trim && el.value.trim() === value) {
return
}
// 如果设了number且值已经是number,返回
if (
(number || el.type === 'number') &&
looseToNumber(el.value) === value
) {
return
}
}
const newValue = value == null ? '' : value
// 新值和旧值不一样才更新
if (el.value !== newValue) {
el.value = newValue
}
}
}

emit的源码位于/package/runtime-core/src/componentEmits.ts,v-model需要和emit配合进行更新。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
if (instance.isUnmounted) return
// 读取当前实例的props对象
const props = instance.vnode.props || EMPTY_OBJ

if (__DEV__) {
const {
emitsOptions,
propsOptions: [propsOptions]
} = instance
if (emitsOptions) {
if (
!(event in emitsOptions) &&
!(
__COMPAT__ &&
(event.startsWith('hook:') ||
event.startsWith(compatModelEventPrefix))
)
) {
if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${toHandlerKey(event)}" prop.`
)
}
} else {
const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...rawArgs)
if (!isValid) {
warn(
`Invalid event arguments: event validation failed for event "${event}".`
)
}
}
}
}
}

let args = rawArgs
// 查看事件是否是update:开头
const isModelListener = event.startsWith('update:')

// for v-model update:xxx events, apply modifiers on args
// 是的话就截取掉,获得后面那段
const modelArg = isModelListener && event.slice(7)
if (modelArg && modelArg in props) {
const modifiersKey = `${
modelArg === 'modelValue' ? 'model' : modelArg
}Modifiers`
// 获取v-model的修饰符,回传的时候也会做处理
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
if (trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
if (number) {
args = rawArgs.map(looseToNumber)
}
}

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentEmit(instance, event, args)
}

if (__DEV__) {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
warn(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(
instance,
instance.type
)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}

// handlerName = on+事件名称
let handlerName
let handler =
props[(handlerName = toHandlerKey(event))] ||
// also try camelCase event handler (#2249)
props[(handlerName = toHandlerKey(camelize(event)))]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
}

// 执行对应的回调函数
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}

const onceHandler = props[handlerName + `Once`]
if (onceHandler) {
if (!instance.emitted) {
instance.emitted = {} as Record<any, boolean>
} else if (instance.emitted[handlerName]) {
return
}
instance.emitted[handlerName] = true
callWithAsyncErrorHandling(
onceHandler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}

if (__COMPAT__) {
compatModelEmit(instance, event, args)
return compatInstanceEmit(instance, event, args)
}
}