Skip to content

v-model 语法糖

Vue2 中的实现

原理

其核心实现分两步:

  1. 使用 v-bind 绑定数据
  2. 触发指定事件修改 v-bind 绑定的数据
html
<input v-bind:value="something" v-on:input="something = $event.target.value">

等价于

html
<input v-model="something">

组件上实现

由于在 input 上自带了 input 事件,所以我们能轻易使用 v-model 语法糖实现双向绑定。那么如果想要在组件上实现 v-model, 子组件必须向父组件抛出 input 事件。

vue
父组件
<template>
    <div>
        <ChildTpl v-model="count" />
        <!-- 等价于 -->
        <ChildTpl :value="count" @input="value = $event" />
    </div>
</template>
vue
子组件
<script>
export default {
    props:{
        value:{
            default:0,
        }
    },
    methods: {
        changeValue(){
            this.$emit('input', this.value+1) // 向父组件抛出 input 事件和最新的值
        }
    }
}
</script>

自定义值和事件

当然上面的方式只支持 value 值和 input 事件,如果我们想自定义属性和事件的话,子组件需要使用到 model 属性

vue
子组件
<script>
export default {
    model: {
        prop: "customParams", // 将默认value属性改为customParams
        event: "change", // 将默认input事件改为change
    },
    props: {
        customParams: {
            type: Number,
            default: 0,
        },
    },
    methods: {
        changeValue() {
            this.$emit("change", this.customParams + 1);
        },
    },
};
</script>
vue
父组件
<template>
    <ChildTpl v-model="count" />
</template>

实现多个v-model

首先明确,Vue2 是不支持实现多个 v-model 语法的。不过 vue 2.3.0 时新增了 sync 修饰符,我们可以借此来实现多个 v-model

vue
子组件
<script>
export default {
    props: {
        title: {
            type: String,
            default: '',
        },
        name: {
            type: String,
            default: '',
        }
    },
    methods: {
        changeValue() {
            this.$emit("update:title", newTitle)
            this.$emit("update:name", newName);
        },
    },
};
</script>
vue
父组件
<template>
    <ChildTpl :title.sync="parentTitle" :name.sync="parentName" />
</template>

注意父组件中不能使用 v-model ,只能是 v-bind:---.sync 这样的写法。

TIP

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。

Vue3 中的实现

Vue3 中的实现和 Vue2 中差不多,其核心在于在组件上的差别。

input 上实现

依然是通过 v-bind 绑定属性,然后通过 input 事件来修改绑定的值。

html
<input :value="searchText" @input="searchText = $event.target.value" />

在组件上实现

父组件

vue
<template>
    <ChildTpl v-model="count" />
    <!-- 等价于 -->
    <ChildTpl :modelValue="count" @update:modelValue="count = $event" />
</template>

子组件

vue
<template>
    <button @click="changeValue">count+1</button>
</template>
<script lang="ts" setup>
const emits = defineEmits(["update:modelValue"])
const props = defineProps({
    modelValue: {
        type: Number,
        default: 0,
    },
});
const changeValue = () => {
    emits("update:modelValue", props.modelValue + 1)
}
</script>

与 Vue2 的区别在于:

  1. 通过 props 来指定 modelValue 绑定的属性
  2. 通过指定的 update:modelValue 事件名来触发事件

自定义值和事件

默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字
父组件:

vue
<template>
    <ChildTpl v-model:num="count" />
    <!-- 等价于 -->
    <ChildTpl :num="count" @update:num="count = $event" />
</template>

子组件:

vue
<template>
    <button @click="changeValue">加一</button>
</template>

<script lang="ts" setup>
const emits = defineEmits(["update:num"])
const props = defineProps({
    num: {
        type: Number,
        default: 0,
    },
})
const changeValue = () => {
    emits("update:num", props.num + 1);
}
</script>

支持多个 v-model

父组件:

vue
<template>
    <ChildTpl v-model:first-name="first" v-model:last-name="last" />
    <!-- 等价于 -->
    <ChildTpl
        :first-name="first"
        :last-name="last"
        @update:first-name="first = $event"
        @update:last-name="last = $event"
    />
</template>

子组件:

vue
<template>
    <input
        type="text"
        :value="firstName"
        @input="$emit('update:firstName', $event.target.value)"
    />
    <input
        type="text"
        :value="lastName"
        @input="$emit('update:lastName', $event.target.value)"
    />
</template>
<script lang="ts" setup>
const emits = defineEmits(['update:firstName', 'update:lastName'])
const props = defineProps({
    firstName: {
        type: String,
        default: '',
    },
    lastName: {
        type: String,
        default: '',
    },
});
</script>