Code前端首页关于Code前端联系我们

先别急着敲代码,Vue3 v-model的核心原理得先摸透

terry 16小时前 阅读数 243 #Vue
文章标签 vmodel原理

Vue3 v-model怎么用?和Vue2比有啥区别?一文讲透双向绑定 要把工具用好,先得懂它是怎么干活的,Vue3里的v-model其实本质没变,还是靠单向数据流的反转语法糖组合——但实现语法糖的代码和Vue2完全不一样了,从组件层面的props+emit,到原生元素的底层处理,都沾了Composition API和Proxy代理的光。

咱们先回忆下原生表单元素的双向绑定,比如input标签,写了v-model="msg"就不用手动监听input事件再赋值给data,对吧?Vue3给原生元素做的底层改造很小,除了checkbox/radio这些多状态控件的默认值绑定逻辑微调了一点点,原理还是监听用户输入触发的事件(原生input、change,部分第三方框架会用自定义事件),同时把响应式数据的值同步到标签的value/checked/selected属性上。

但组件层面的变化才是关键,以前Vue2是强制一个组件只能有一个v-model,而且对应props是value,emit是input;如果要做多个双向绑定的属性,得用.sync修饰符,对应props是:title.sync这种,emit是update:title,现在Vue3直接把.sync的语法糖整合进了v-model,强制去掉了旧版的唯一value/input限制,相当于所有组件的双向绑定属性,都可以用v-model:propName的形式写,默认的prop名变成了modelValue,默认的emit名变成了update:modelValue——这就解决了很多场景下组件只能传一个核心双向值的痛点,比如一个弹窗组件,既要双向控制显示隐藏(v-model:visible),又要双向控制输入框内容(v-model:content),现在一行搞定,不用再写一堆.sync修饰符的冗余代码。

新手必看:原生表单元素的Vue3 v-model怎么正确写?

虽然和Vue2底层变化不大,但默认绑定的事件或者属性还是有几个新手容易踩坑的地方,咱们一个一个常用控件说:

文本输入框、多行文本域(input text、textarea)

这个是最基础的,用法和Vue2一模一样,直接给v-model绑定一个响应式字符串就行,不管是Composition API里的ref还是reactive包裹的对象属性,都能直接用。

import { ref, reactive } from 'vue'
const inputText = ref('你好,Vue3')
const formData = reactive({
  textareaContent: '这是多行文本'
})
<!-- 绑定ref -->
<input type="text" v-model="inputText" />
<!-- 绑定reactive对象属性 -->
<textarea v-model="formData.textareaContent"></textarea>

这里要注意一个小细节:Vue3不会像有些旧框架那样,自动帮你把textarea的内容用\r\n或者\n统一换行,不过现在主流浏览器都能兼容不同的换行符,一般不用特意处理。

复选框(input checkbox)

复选框分两种使用场景:单个开关(比如同意用户协议),或者多个选中项组成数组(比如选择兴趣爱好),两种场景的绑定方式和响应式数据类型要对应上,不然会报错或者数据乱跳。

单个开关的话,绑定ref或reactive里的布尔值就行,Vue3会自动把checked属性和响应式布尔值同步,点击的时候触发update事件改值。

const agree = ref(false)
<input type="checkbox" id="agree" v-model="agree" />
<label for="agree">我已阅读并同意用户协议</label>

多个选中项的话,必须绑定ref或reactive里的数组,每个复选框要加value属性,Vue3会把选中的value值自动塞进数组,取消的话自动删掉。

const hobbies = ref(['读书', '看电影'])
<input type="checkbox" id="read" value="读书" v-model="hobbies" />
<label for="read">读书</label>
<input type="checkbox" id="movie" value="看电影" v-model="hobbies" />
<label for="movie">看电影</label>
<input type="checkbox" id="game" value="打游戏" v-model="hobbies" />
<label for="game">打游戏</label>

单选框(input radio)

单选框的绑定就一个要求:绑定的响应式数据必须是单个值(字符串、数字、布尔值都行),每个单选框也要加value属性,Vue3会自动把选中的value值赋给响应式数据。

const gender = ref('女')
<input type="radio" id="male" value="男" v-model="gender" />
<label for="male">男</label>
<input type="radio" id="female" value="女" v-model="gender" />
<label for="female">女</label>

下拉选择框(select)

下拉选择框也分两种场景:单选和多选,多选的话必须给select标签加multiple属性,对应的响应式数据类型也要变成数组。

单选下拉框,绑定单个值就行,option标签可以加value属性(支持字符串、数字、布尔值、甚至对象,但如果是对象的话,Vue3会做严格相等比较),也可以不加,不加的话Vue3会用option标签的文本内容作为value。

const city = ref('上海')
<select v-model="city">
  <option value="北京">北京</option>
  <option value="上海">上海</option>
  <option value="广州">广州</option>
</select>

多选下拉框,绑定数组,加multiple属性:

const cities = ref(['上海', '广州'])
<select v-model="cities" multiple>
  <option value="北京">北京</option>
  <option value="上海">上海</option>
  <option value="广州">广州</option>
</select>

修饰符还是那几个,但用法得留意变化

Vue3保留了Vue2里的三个常用v-model修饰符:.lazy、.number、.trim,但去掉了.sync修饰符的独立写法,另外新增了自定义修饰符的功能,这个咱们后面讲组件双向绑定的时候再说。

.lazy修饰符的作用是把默认监听的input事件换成change事件,也就是说只有当输入框失焦或者按回车的时候,响应式数据才会更新,适合一些输入完才需要验证或者提交的场景。

<!-- 只有失焦或按回车,lazyText才会变 -->
<input type="text" v-model.lazy="lazyText" />

.number修饰符的作用是自动把输入框的内容转换成数字,如果转换失败(比如输入了“abc123”),就会保留原来的字符串,这里要注意:如果输入框本身是type="number"的话,不管加不加.number,有些浏览器会自动返回数字,但为了兼容性,最好还是加上。

<!-- 自动转数字 -->
<input type="text" v-model.number="age" />

.trim修饰符的作用是自动去掉输入框内容的首尾空格,中间的空格不会动,适合用户名、密码这种不允许有首尾空格的场景。

<!-- 自动去首尾空格 -->
<input type="text" v-model.trim="username" />

修饰符还可以组合使用,比如同时用.lazy和.trim,顺序不影响效果:

<input type="text" v-model.lazy.trim="username" />

进阶重点:组件层面的Vue3 v-model怎么封装和使用?

这应该是从Vue2转过来的人最关心的部分,也是新手进阶的必经之路,刚才说了,Vue3整合了.sync,所以组件可以有多个v-model绑定属性,默认的prop名和emit名都变了,咱们先讲单个默认v-model的封装,再讲多个自定义v-model的,最后讲自定义修饰符。

单个默认v-model的封装

单个默认v-model的话,组件内部只需要接收一个叫modelValue的prop,触发一个叫update:modelValue的emit就行,不用再像Vue2那样在组件里配置model选项了。

举个最简单的例子:封装一个自定义的输入框组件,里面加个边框、加个占位符提示,就用单个默认v-model。 首先写组件的逻辑部分:

import { defineProps, defineEmits } from 'vue'
// 定义接收的prop,modelValue类型可以是字符串、数字、布尔值,根据需求来
const props = defineProps(['modelValue', 'placeholder'])
// 定义触发的emit,必须包含update:modelValue
const emit = defineEmits(['update:modelValue'])
// 写一个中间处理函数,方便后续加验证或者其他逻辑
const handleInput = (e) => {
  // 触发update:modelValue事件,把输入框的值传出去
  emit('update:modelValue', e.target.value)
}

然后写组件的模板部分:

<div class="custom-input-wrapper">
  <input
    type="text"
    :value="modelValue"
    :placeholder="placeholder"
    @input="handleInput"
    class="custom-input"
  />
</div>

最后在父组件里使用:

import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const customText = ref('这是自定义输入框的内容')
<CustomInput v-model="customText" placeholder="请输入内容" />

你看,是不是比Vue2简单多了?不用再记value和input,也不用配置model选项。

多个自定义v-model的封装

这个才是Vue3的亮点所在,刚才提到的弹窗组件就是典型场景:既要双向控制显示隐藏,又要双向控制输入框内容,咱们就来封装这个弹窗组件。

首先写组件的逻辑部分:

import { defineProps, defineEmits } from 'vue'
// 定义接收的prop,两个:visible(控制显示隐藏的布尔值)、content(控制输入框的字符串)
const props = defineProps({
  visible: Boolean,
  content: String, String // 这个不是双向绑定的,是普通props
})
// 定义触发的emit,两个:update:visible、update:content
const emit = defineEmits(['update:visible', 'update:content'])
// 关闭弹窗的函数
const closeModal = () => {
  emit('update:visible', false)
}
// 输入框的处理函数
const handleContentInput = (e) => {
  emit('update:content', e.target.value)
}

然后写组件的模板部分:

<div v-if="visible" class="modal-mask">
  <div class="modal-content">
    <h3>{{ title }}</h3>
    <input
      type="text"
      :value="content"
      @input="handleContentInput"
      placeholder="请输入弹窗内容"
    />
    <div class="modal-footer">
      <button @click="closeModal">取消</button>
      <button @click="closeModal">确定</button>
    </div>
  </div>
</div>

最后在父组件里使用:

import { ref } from 'vue'
import CustomModal from './CustomModal.vue'
const modalVisible = ref(false)
const modalContent = ref('')
<button @click="modalVisible = true">打开弹窗</button>
<!-- 两个v-model,一个绑定visible,一个绑定content -->
<CustomModal
  v-model:visible="modalVisible"
  v-model:content="modalContent""自定义弹窗"
/>

看到没?一行代码绑定两个双向属性,比Vue2写两个:visible.sync、:content.sync清晰多了,代码也更简洁。

自定义修饰符的封装

这个是Vue3的另一个小亮点,虽然用的场景可能不是特别多,但有时候能省很多事,比如咱们刚才封装的自定义输入框组件,想要一个.currency修饰符,自动把输入的内容加上¥前缀,再保留两位小数,怎么实现?

v-model的修饰符会自动作为一个叫modelModifiers的prop传给组件(如果是自定义v-model:xxx的话,prop名就是xxxModifiers),这个prop是一个对象,key是修饰符的名字,value是true(如果用了的话)或者undefined(如果没用的话)。

咱们来修改刚才的CustomInput.vue组件: 逻辑部分:

import { defineProps, defineEmits } from 'vue'
// 这里要加modelModifiers,默认值是一个空对象
const props = defineProps({
  modelValue: [String, Number],
  placeholder: String,
  modelModifiers: {
    default: () => ({})
  }
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
  let value = e.target.value
  // 检查有没有用.currency修饰符
  if (props.modelModifiers.currency) {
    // 去掉所有非数字和小数点的字符
    value = value.replace(/[^\d.]/g, '')
    // 保留两位小数
    const decimalIndex = value.indexOf('.')
    if (decimalIndex !== -1) {
      value = value.slice(0, decimalIndex + 3)
    }
    // 加上¥前缀(如果输入为空的话就不加)
    value = value ? `¥${value}` : ''
  }
  emit('update:modelValue', value)
}

模板部分不用改,直接在父组件里用:

import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const price = ref('¥0.00')
<CustomInput v-model.currency="price" placeholder="请输入价格" />

这样一来,用户输入的时候,非数字和小数点的字符会被自动过滤,保留两位小数,还会自动加上¥前缀,是不是很方便?

Vue2 v-model迁移到Vue3的踩坑指南

从Vue2转Vue3的人,最容易踩的坑就是组件双向绑定的部分,咱们总结一下常见的问题:

组件内部还在接收value/触发input,或者还在配置model选项

如果是旧的Vue2组件迁移过来,第一件事就是把接收的prop从value改成modelValue(如果是默认v-model的话),把触发的emit从input改成update:modelValue;如果是多个.sync修饰符的属性,直接把.sync去掉,改成v-model:propName,组件内部接收propName,触发update:propName;model选项在Vue3里已经完全废弃了,必须删掉。

忘了给自定义v-model的prop加类型或者默认值

虽然defineProps里可以只传一个数组,但建议最好传一个对象,给每个prop加类型和默认值,这样可以避免很多类型错误,也有利于代码的可维护性,特别是modelModifiers这种prop,默认值必须是一个空对象,不然访问的时候会报错。

响应式数据类型和控件/组件要求的不匹配

比如单个复选框绑定了数组,多个选中项绑定了单个值,多选下拉框没加multiple属性却绑定了数组,这些都会导致数据乱跳或者报错,新手和迁移用户都要特别注意。

还在使用.sync修饰符

虽然有些Vue3的兼容版本(比如Vue 3.2之前的某些版本)可能还支持.sync,但官方已经明确废弃了,所以最好直接改成v-model:propName的形式,避免以后升级的时候出问题。

实战场景:用Vue3 v-model封装一个可复用的搜索筛选组件

最后咱们来一个实战场景,封装一个可复用的搜索筛选组件,包含两个输入框(搜索关键词和搜索时间范围的开始时间)、一个下拉选择框(筛选状态),还有一个“重置”按钮,点击重置按钮可以把所有双向绑定的属性恢复到初始值,这个组件会用到多个自定义v-model,还有普通props、普通emit,比较全面。

首先写组件的逻辑部分,SearchFilter.vue:

import { defineProps, defineEmits, watch } from 'vue'
// 定义接收的prop
const props = defineProps({
  // 搜索关键词,双向绑定
  keyword: {
    type: String,
    default: ''
  },
  // 筛选状态,双向绑定
  status: {
    type: String,
    default: 'all'
  },
  // 开始时间,双向绑定
  startTime: {
    type: String,
    default: ''
  },
  // 状态下拉框的选项,普通prop
  statusOptions: {
    type: Array,
    default: () => [
      { label: '全部', value: 'all' },
      { label: '进行中', value: 'ongoing' },
      { label: '已完成', value: 'completed' },
      { label: '已取消', value: 'cancelled' }
    ]
  }
})
// 定义触发的emit
const emit = defineEmits([
  'update:keyword',
  'update:status',
  'update:startTime',
  'search', // 点击搜索按钮触发的普通emit
  'reset' // 点击重置按钮触发的普通emit
])
// 搜索的处理函数
const handleSearch = () => {
  emit('search')
}
// 重置的处理函数,这里我们可以直接在组件内部重置吗?
// 不行,因为Vue3是单向数据流,组件内部不能直接修改props,
// 所以要触发reset事件,让父组件自己重置响应式数据
const handleReset = () => {
  emit('reset')
}
// 监听输入框的变化,自动触发搜索?
// 可以,但这里为了演示普通emit,我们还是加个搜索按钮
// 不过如果需要实时搜索,可以用watch
// watch([() => props.keyword, () => props.status, () => props.startTime], () => {
//   emit('search')
// })

然后写组件的模板部分:

<div class="search-filter-wrapper">
  <div class="filter-item">
    <label>搜索关键词:</label>
    <input
      type="text"
      :value="keyword"
      @input="(e) => emit('update:keyword', e.target.value)"
      placeholder="请输入关键词"
    />
  </div>
  <div class="filter-item">
    <label>筛选状态:</label>
    <select
      :value="status"
      @change="(e) => emit('update:status', e.target.value)"
    >
      <option
        v-for="option in statusOptions"
        :key="option.value"
        :value="option.value"
      >
        {{ option.label }}
      </option>
    </select>
  </div>
  <div class="filter-item">
    <label>开始时间:</label>
    <input
      type="date"
      :value="startTime"
      @change="(e) => emit('update:startTime', e.target.value)"
    />
  </div>
  <div class="filter-buttons">
    <button class="search-btn" @click="handleSearch">搜索</button>
    <button class="reset-btn" @click="handleReset">重置</button>
  </div>
</div>
<style scoped>
.search-filter-wrapper {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 8px;
  margin-bottom: 16px;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.filter-item label {
  font-size: 14px;
  color: #333;
}
.filter-item input,
.filter-item select {
  padding: 6px 12px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
}
.filter-buttons {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}
.search-btn,
.reset-btn {
  padding: 6px 16px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
}
.search-btn {
  background-color: #409eff;
  color: #fff;
}
.search-btn:hover {
  background-color: #66b1ff;
}
.reset-btn {
  background-color: #fff;
  color: #606266;
  border: 1px solid #dcdfe6;
}
.reset-btn:hover {
  border-color: #409eff;
  color: #409eff;
}
</style>

最后在父组件里使用:

import { ref, reactive } from 'vue'
import SearchFilter from './SearchFilter.vue'
// 定义筛选条件的响应式数据
const filterParams = reactive({
  keyword: '',
  status: 'all',
  startTime: ''
})
// 保存初始值,用于重置
const initialFilterParams = { ...filterParams }
// 搜索的处理函数
const handleSearch = () => {
  console.log('搜索条件:', filterParams)
  // 这里可以写调用后端接口的代码
}
// 重置的处理函数
const handleReset = () => {
  // 把filterParams恢复到初始值
  Object.assign(filterParams, initialFilterParams)
  console.log('筛选条件已重置')
  // 重置后可以自动触发搜索,也可以不触发,根据需求来
  handleSearch()
}
<SearchFilter
  v-model:keyword="filterParams.keyword"
  v-model:status="filterParams.status"
  v-model:startTime="filterParams.startTime"
  @search="handleSearch"
  @reset="handleReset"
/>

这样一个可复用的搜索筛选组件就封装好了,父组件可以根据自己的需求调整statusOptions的内容,也可以加更多的筛选条件,比如结束时间、分页等,只需要在组件里加对应的prop和emit就行。

Vue3 v-model的核心原理还是单向数据流的反转加语法糖组合,但实现方式比Vue2更灵活、更简洁:整合了.sync修饰符,支持多个自定义v-model绑定属性,默认的prop名和emit名更规范,还新增了自定义修饰符的功能,新手要先掌握原生表单元素的用法和常见的修饰符,进阶用户要重点掌握组件层面的封装和使用,特别是多个自定义v-model和自定义修饰符的部分,从Vue2迁移过来的用户,要注意废弃的model选项和.sync修饰符,及时修改组件内部的prop和emit,多做实战练习,把这些知识用到实际项目中,才能真正掌握Vue3 v-model的用法。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门