v-model 在 Vue2 和 Vue3 中的区别

8,064 阅读3分钟

v-model 是 Vue 中使用频率特别高的一个指令,而 Vue3 中的 v-model 有了很大的变化,本文将详细讲述一下 Vue2 和 Vue3 中的 v-model 的区别。

Vue2 中的 v-model

如果对 Vue2 中的语法很熟悉,这部分可以不看。

首先来回顾一下 Vue2 中的 v-model,它主要用于表单元素和自定义组件上。v-model本质上是一个语法糖,会对用户的输入做一些特殊处理以达到更新数据,而所谓的处理其实就是给使用的元素默认绑定属性和事件。

v-model 使用在表单元素上时,会根据元素的不同而采用不同的处理:

  • <input type="text">文本<textarea>上使用时,会默认给元素绑定名为 value 的 prop 和名为 input 的事件;
  • <input type="checkbox">复选框<input type="radio">单选框 上使用时,会默认绑定名为 checked 的 prop 和名为 change 的事件;
  • <select>选择框 上使用时,则绑定名为 value 的 prop 和名为 change 的事件。

这些是 Vue 默认帮我们处理的,可以直接使用。但是你也会发现一些第三方组件也可以使用 v-model ,比如 Element 中的 Input 组件。这是因为这些组件自己实现了 v-model,原理其实就是上面说到的绑定属性和事件。

我们可以尝试实现一下 v-model,来开发一个简单的输入组件,就叫 MyInput 吧:

<!-- MyInput 组件代码 -->

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

<script>
export default {
  props: {
    value: String,  // 默认接收一个名为 value 的 prop
  }
}
</script>

上面代码就实现了组件的 v-model 功能,当在这个组件上使用 v-model 时:

<my-input v-model="msg"></my-input>

其实就等同于:

<my-input :value="msg" @input="msg = $event">

Vue 还提供了 model 选项,用于将属性或事件名称改为其他名称,比如上面的 MyInput 组件,我们改一下:

<template>
  <div>
    <input
      type="text"
      :value="title"
      @input="$emit('change', $event.target.value)"
    />
  </div>
</template>

<script>
export default {
  model: {
    prop: "title", // 将默认的 prop 名 value 改为 title
    event: "change", // 将默认的事件名 input 改为 change
  },
  props: {
    title: String, // 注意 template 代码中也要修改为 title
  },
};
</script>

此时使用组件:

<my-input v-model="msg"></my-input>

// 等同于
<my-input :title="msg" @change="msg = $event"></my-input>

使用 .sync 修饰符

Vue 提供一个 .sync 的修饰符,效果跟 v-model 一样,也是便于子组件数据更改后自动更新父组件相关数据。实现 .sync 的方式与实现 v-model 异曲同工,区别就是抛出的事件名需要是 update:myPropName 的结构。

还是拿上面的 MyInput 说明,我们还是传入一个 title 的 prop,同时组件内部抛出 update:title 事件,代码如下:

// MyInput 组件中,修改抛出的事件名为 update:title
 <input type="text" :value="title" @input="$emit('update:title', $event.target.value)" />

此时如果使用这个组件,正常应该是这样:

<my-input :title="msg" @update:title="msg = $event"></my-input>

但此时可以使用 .sync 修饰符来简化:

<my-input :title.sync="msg"></my-input>

可以看到 .syncv-model 所能达到的效果是一样的,用什么就看你什么场景,一般表单组件上都是用 v-model

Vue3 中的 v-model

上面说了那么多,为的就是接下来区别出 Vue3 中 v-model 带来的变化,主要变化有以下几处:

修改默认 prop 名和事件名

当用在自定义组件上时,v-model 默认绑定的 prop 名从 value 变为 modelValue,而事件名也从默认的input 改为 update:modelValue 。在 Vue3 中编写上面那个 MyInput 组件时,就需要这样:

<!-- MyInput 组件代码 Vue3 版 -->

<template>
  <div>
    <input
      type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"  // 事件名改为 update:modelValue
    />
  </div>
</template>

<script>
export default {
  props: {
    modelValue: String, // 默认 prop 从 value 改为 modelValue
  },
};
</script>

使用组件时:

<my-input v-model="msg"></my-input>

// 等同于
<my-input :modelValue="msg" @update:modelValue="msg = $event"></my-input>

废除 model 选项和 .sync 修饰符

Vue3 中移除了 model 选项,这样就不可以在组件内修改默认 prop 名了。现在有一种更简单的方式,就是直接在 v-model 后面传递要修改的 prop 名:

// 要修改默认 prop 名,只需在 v-model 后面接上 :propName,例如修改为 title
<my-input v-model:title="msg"></my-input>

// 等同于
<my-input :title="msg" @update:title="msg = $event"></my-input>

注意组件内部也要修改 props:

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

<script>
export default {
  // 此时这里不需要 model 选项来修改了
  props: {
    title: String, // 修改为 title,注意 template 中也要修改
  },
};
</script>

同时,.sync 修饰符也被移除了,如果你尝试使用它,会报这样的错误:

'.sync' modifier on 'v-bind' directive is deprecated. Use 'v-model:propName' instead

错误提示中说明了,可以使用 v-model:propName 的方式来替代 .sync,因为本质上效果是一样的。

使用多个 v-model

Vue3 中支持使用多个 v-model,属于新增功能,我很喜欢这个功能,使得组件数据更新更灵活。例如有这样一个表单子组件,用户输入的多个数据都需要更新到父组件中显示,可以这样写:

<!--  表单子组件 Form -->

<template>
  <div class="form">
    
    <label for="name">姓名</label>
    <input id="name" type="text" :value="name" @input="$emit('update:name',$event.target.value)">
    
    <label for="address">地址</label>
    <input id="address" type="text" :value="address" @input="$emit('update:address',$event.target.value)">
  
  </div>
</template>

<script>
export default {
  props:{
    name: String,
    address: String
  }
}
</script>

父组件使用这个组件时:

<child-component v-model:name="name" v-model:address="address"></child-component>
    
// 将用户输入数据更新到父组件中显示
<p>{{name}}</p>
<p>{{address}}</p>

自定义 v-model 修饰符

在 Vue2 中的 v-model 上,我们用过 .trim.lazy.number这三个内置修饰符,而 Vue3 则在这个基础上增加了自定义修饰符,即开发者可以自定义修饰符,以按需处理绑定值。

当我们在 v-model 后面加上自定义修饰符后,会通过名为 modelModifiers 的 prop 传递给子组件,子组件拿到这个修饰符名后,根据条件修改绑定值。我们来看一个例子,自定义一个修饰符 capitalize,用于将输入字符串的首字母大写。

假设自定义组件还是叫 MyInput,使用 v-model 时加上自定义修饰符 capitalize

<my-input v-model.capitalize="msg"></my-input>

由于不是内置修饰符,所以需要我们自己在组件内部处理修饰符逻辑,编写组件:

<!-- MyInput 组件 -->

<template>
  <div>
    <input type="text" :value="modelValue" @input="emitValue" />
  </div>
</template>

<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {  // 自定义修饰符会默认传入这个 prop 中
      type: Object,
      default: () => ({}),
    },
  },
  mounted() {
    // 当组件 v-model 后面加上了自定义修饰符,组件内部会在 modelModifiers 上获取到修饰符状态
    console.log(this.modelModifiers); // {capitalize: true}
  },
  methods: {
    emitValue(e) {
      let value = e.target.value;
      // 如果使用了自定义修饰符,即状态为 true,就处理值
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1);
      }
      // emit value
      this.$emit("update:modelValue", value);
    },
  },
};
</script>

这样就完成了一个将输入字符串首字母大写的v-model修饰符。

如果是 v-model 带上了参数,同时使用了自定义修饰符,比如这样:

<my-input v-model:title.capitalize="msg"></my-input>

那么传入组件内部的 prop 就不再是 modelModifiers 了,而是 titleModifiers。它的格式是 arg + 'Modifiers'。此时这个组件应该这样写:

<!-- MyInput 组件 -->

<template>
  <div>
    <input type="text" :value="title" @input="emitValue" />
  </div>
</template>

<script>
export default {
  props: {
    title: String,  // modelValue -> title
    titleModifiers: {  // modelModifiers -> titleModifiers
      type: Object,
      default: () => ({}),
    },
  },
  mounted() {
    console.log(this.titleModifiers); // {capitalize: true}
  },
  methods: {
    emitValue(e) {
      let value = e.target.value;

      // 如果使用了自定义修饰符,就处理值
      if (this.titleModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1);
      }
      // emit value
      this.$emit("update:title", value);
    },
  },
};
</script>

结语

以上就是 v-model 在 Vue2 和 Vue3 中的区别表现,我个人觉得新版中使用逻辑更清晰了。只是有些部分不再兼容了,如果是直接将 Vue2 版本代码移植到 Vue3 项目中,需要注意 v-model 的实现差异,还有注意要将 .sync 修饰符替换成新写法。

文中如有不当之处,欢迎评论区指出。