前端八股6---v-model双向绑定

目录

  • 一、核心概念
  • [1.1 什么是双向绑定?](#1.1 什么是双向绑定?)
  • [1.2 v-model 的本质](#1.2 v-model 的本质)
  • [二、在表单元素上使用 v-model](#二、在表单元素上使用 v-model)
  • [2.1 输入框 text / textarea](#2.1 输入框 text / textarea)
  • [2.2 复选框 checkbox](#2.2 复选框 checkbox)
  • [2.3 单选框 radio](#2.3 单选框 radio)
  • [2.4 下拉框 select](#2.4 下拉框 select)
  • [三、v-model 的修饰符](#三、v-model 的修饰符)
  • [3.1 .lazy - 改为 change 事件触发](#3.1 .lazy - 改为 change 事件触发)
  • [3.2 .number - 自动转为数字](#3.2 .number - 自动转为数字)
  • [3.3 .trim - 自动去除首尾空格](#3.3 .trim - 自动去除首尾空格)
  • [3.4 链式使用](#3.4 链式使用)
  • [四、在自定义组件上使用 v-model](#四、在自定义组件上使用 v-model)
  • [4.1 单个 v-model(默认)](#4.1 单个 v-model(默认))
  • [4.2 多个 v-model(绑定不同属性)](#4.2 多个 v-model(绑定不同属性))
  • [4.3 带类型的 TypeScript 版本](#4.3 带类型的 TypeScript 版本)
  • [4.4 自定义 v-model 的名称和事件](#4.4 自定义 v-model 的名称和事件)
  • [五、v-model 的高级用法](#五、v-model 的高级用法)
  • [5.1 结合 computed 使用](#5.1 结合 computed 使用)
  • [5.2 自定义组件的 v-model 修饰符](#5.2 自定义组件的 v-model 修饰符)
  • [5.3 多个 v-model 的修饰符](#5.3 多个 v-model 的修饰符)
  • 六、完整实战示例:地址表单组件
  • 七、面试高频问题
  • [Q1:v-model 的原理是什么?](#Q1:v-model 的原理是什么?)
  • [Q2:v-model 和 .sync 修饰符的关系?](#Q2:v-model 和 .sync 修饰符的关系?)
  • [Q3:如何在一个组件上使用多个 v-model?](#Q3:如何在一个组件上使用多个 v-model?)
  • [Q4:v-model 和 ref 的区别?](#Q4:v-model 和 ref 的区别?)
  • 八、快速记忆

一、核心概念

1.1 什么是双向绑定?

单向数据流: 数据变化 → 视图更新(但视图变化不会自动改数据)

双向绑定: 数据变化 → 视图更新 并且 视图变化 → 数据自动更新

复制代码
单向: 数据  ——→  视图
双向: 数据  ←→  视图

1.2 v-model 的本质

html 复制代码
<!-- v-model 写法 -->
<input v-model="username" />

<!-- 本质是语法糖,等价于: -->
<input :value="username" @input="username = $event.target.value" />

拆解:

  • :value="username" → 把数据绑定到输入框的 value 属性(数据 → 视图)

  • @input="username = $event.target.value" → 输入时把新值赋给数据(视图 → 数据)


二、在表单元素上使用 v-model

2.1 输入框 text / textarea

html 复制代码
<template>
  <!-- 文本输入框 -->
  <input v-model="message" />
  <textarea v-model="description" />
  
  <!-- 等价写法 -->
  <input :value="message" @input="message = $event.target.value" />
</template>

<script setup>
import { ref } from 'vue'
const message = ref('')
const description = ref('')
</script>

2.2 复选框 checkbox

html 复制代码
<template>
  <!-- 单个复选框:绑定布尔值 -->
  <input type="checkbox" v-model="isAgreed" />
  <p>是否同意:{{ isAgreed }}</p>
  
  <!-- 多个复选框:绑定数组 -->
  <input type="checkbox" value="苹果" v-model="fruits" />
  <input type="checkbox" value="香蕉" v-model="fruits" />
  <input type="checkbox" value="橙子" v-model="fruits" />
  <p>选中的水果:{{ fruits }}</p>
</template>

<script setup>
import { ref } from 'vue'
const isAgreed = ref(false)
const fruits = ref([])
</script>

2.3 单选框 radio

html 复制代码
<template>
  <input type="radio" value="男" v-model="gender" />
  <input type="radio" value="女" v-model="gender" />
  <p>性别:{{ gender }}</p>
</template>

<script setup>
import { ref } from 'vue'
const gender = ref('')
</script>

2.4 下拉框 select

html 复制代码
<template>
  <!-- 单选 -->
  <select v-model="city">
    <option value="北京">北京</option>
    <option value="上海">上海</option>
    <option value="广州">广州</option>
  </select>
  
  <!-- 多选(加 multiple 属性) -->
  <select v-model="cities" multiple>
    <option value="北京">北京</option>
    <option value="上海">上海</option>
    <option value="广州">广州</option>
  </select>
</template>

<script setup>
import { ref } from 'vue'
const city = ref('')
const cities = ref([])
</script>

三、v-model 的修饰符

3.1 .lazy - 改为 change 事件触发

html 复制代码
<!-- 默认:input 事件(实时触发) -->
<input v-model="username" />

<!-- .lazy:改为 change 事件(失焦或回车时触发) -->
<input v-model.lazy="username" />

3.2 .number - 自动转为数字

html 复制代码
<!-- 即使输入"123",也会自动转为数字 123 -->
<input v-model.number="age" type="number" />

3.3 .trim - 自动去除首尾空格

html 复制代码
<!-- 输入"  张三  " 会变成 "张三" -->
<input v-model.trim="username" />

3.4 链式使用

html 复制代码
<!-- 可以组合使用 -->
<input v-model.trim.lazy="username" />
<input v-model.number="age" />

四、在自定义组件上使用 v-model

4.1 单个 v-model(默认)

父组件:

html 复制代码
<template>
  <!-- 使用 v-model -->
  <CustomInput v-model="username" />
  
  <!-- 本质是: -->
  <CustomInput 
    :modelValue="username" 
    @update:modelValue="username = $event" 
  />
</template>

<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const username = ref('张三')
</script>

子组件 CustomInput.vue:

html 复制代码
<template>
  <div>
    <!-- 接收 props.value,触发 update:modelValue 事件 -->
    <input 
      type="text"
      :value="modelValue"
      @input="emit('update:modelValue', $event.target.value)"
    />
  </div>
</template>

<script setup>
// 接收默认的 modelValue
defineProps(['modelValue'])

// 声明 update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
</script>

4.2 多个 v-model(绑定不同属性)

父组件:

html 复制代码
<template>
  <!-- 多个 v-model,指定不同的绑定名 -->
  <UserForm 
    v-model:firstName="firstName"
    v-model:lastName="lastName"
    v-model:age="age"
  />
  
  <!-- 本质是: -->
  <UserForm 
    :firstName="firstName"
    :lastName="lastName"
    :age="age"
    @update:firstName="firstName = $event"
    @update:lastName="lastName = $event"
    @update:age="age = $event"
  />
</template>

<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'

const firstName = ref('张')
const lastName = ref('三')
const age = ref(18)
</script>

子组件 UserForm.vue:

html 复制代码
<template>
  <div>
    <input 
      type="text"
      :value="firstName"
      @input="emit('update:firstName', $event.target.value)"
      placeholder="姓"
    />
    
    <input 
      type="text"
      :value="lastName"
      @input="emit('update:lastName', $event.target.value)"
      placeholder="名"
    />
    
    <input 
      type="number"
      :value="age"
      @input="emit('update:age', parseInt($event.target.value))"
      placeholder="年龄"
    />
  </div>
</template>

<script setup>
// 接收多个 props
defineProps(['firstName', 'lastName', 'age'])

// 声明多个更新事件
const emit = defineEmits(['update:firstName', 'update:lastName', 'update:age'])
</script>

4.3 带类型的 TypeScript 版本

html 复制代码
<script setup lang="ts">
// 定义 props 类型
interface Props {
  firstName: string
  lastName: string
  age: number
}

defineProps<Props>()

// 定义 emit 类型
const emit = defineEmits<{
  'update:firstName': [value: string]
  'update:lastName': [value: string]
  'update:age': [value: number]
}>()
</script>

4.4 自定义 v-model 的名称和事件

html 复制代码
<!-- 父组件:可以自定义绑定的属性名 -->
<CustomInput v-model:title="bookTitle" />

<!-- 子组件需要对应接收 -->
<script setup>
defineProps(['title'])
const emit = defineEmits(['update:title'])
</script>

五、v-model 的高级用法

5.1 结合 computed 使用

html 复制代码
<template>
  <!-- 在子组件内部使用 computed 包装 v-model -->
  <input v-model="localValue" />
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

// 创建一个可读可写的 computed
const localValue = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

5.2 自定义组件的 v-model 修饰符

html 复制代码
<!-- 父组件:使用自定义修饰符 -->
<CustomInput v-model.capitalize="username" />

<!-- 子组件:接收修饰符 -->
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: {  // 接收修饰符
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:modelValue'])

function onInput(e) {
  let value = e.target.value
  
  // 如果有 capitalize 修饰符,首字母大写
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  
  emit('update:modelValue', value)
}
</script>

5.3 多个 v-model 的修饰符

html 复制代码
<!-- 父组件 -->
<ChildComponent
  v-model:title.capitalize="pageTitle"
  v-model:content.uppercase="pageContent"
/>

<!-- 子组件 -->
<script setup>
const props = defineProps({
  title: String,
  titleModifiers: { type: Object, default: () => ({}) },
  content: String,
  contentModifiers: { type: Object, default: () => ({}) }
})

const emit = defineEmits(['update:title', 'update:content'])

function onTitleInput(e) {
  let value = e.target.value
  if (props.titleModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:title', value)
}

function onContentInput(e) {
  let value = e.target.value
  if (props.contentModifiers.uppercase) {
    value = value.toUpperCase()
  }
  emit('update:content', value)
}
</script>

六、完整实战示例:地址表单组件

html 复制代码
<!-- 父组件 App.vue -->
<template>
  <div>
    <h2>地址信息</h2>
    <p>省份:{{ address.province }}</p>
    <p>城市:{{ address.city }}</p>
    <p>区县:{{ address.district }}</p>
    <p>详细地址:{{ address.detail }}</p>
    
    <AddressForm 
      v-model:province="address.province"
      v-model:city="address.city"
      v-model:district="address.district"
      v-model:detail="address.detail"
    />
  </div>
</template>

<script setup>
import { reactive } from 'vue'
import AddressForm from './AddressForm.vue'

const address = reactive({
  province: '',
  city: '',
  district: '',
  detail: ''
})
</script>
html 复制代码
<!-- 子组件 AddressForm.vue -->
<template>
  <div class="address-form">
    <div class="form-group">
      <label>省份</label>
      <select :value="province" @change="emit('update:province', $event.target.value)">
        <option value="">请选择</option>
        <option value="广东">广东</option>
        <option value="北京">北京</option>
        <option value="上海">上海</option>
      </select>
    </div>
    
    <div class="form-group">
      <label>城市</label>
      <input 
        type="text"
        :value="city"
        @input="emit('update:city', $event.target.value)"
        placeholder="请输入城市"
      />
    </div>
    
    <div class="form-group">
      <label>区县</label>
      <input 
        type="text"
        :value="district"
        @input="emit('update:district', $event.target.value)"
        placeholder="请输入区县"
      />
    </div>
    
    <div class="form-group">
      <label>详细地址</label>
      <textarea 
        :value="detail"
        @input="emit('update:detail', $event.target.value)"
        rows="3"
        placeholder="请输入详细地址"
      ></textarea>
    </div>
  </div>
</template>

<script setup>
defineProps(['province', 'city', 'district', 'detail'])

const emit = defineEmits([
  'update:province', 
  'update:city', 
  'update:district', 
  'update:detail'
])
</script>

<style scoped>
.address-form {
  border: 1px solid #ccc;
  padding: 20px;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input, select, textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
</style>

七、面试高频问题

Q1:v-model 的原理是什么?

v-model 是语法糖,本质是 props + emit 。在表单元素上,它绑定 value 属性并监听 input 事件;在自定义组件上,默认绑定 modelValue prop 并监听 update:modelValue 事件。

Q2:v-model 和 .sync 修饰符的关系?

Vue 2 中有 .sync 修饰符用于双向绑定,Vue 3 中 .sync 被废弃,统一用 v-model 并支持多个绑定。

vue

复制代码
<!-- Vue 2 写法 -->
<Child :title.sync="pageTitle" />

<!-- Vue 3 写法(等价) -->
<Child v-model:title="pageTitle" />

Q3:如何在一个组件上使用多个 v-model?

通过指定不同的绑定名:v-model:propName="data",子组件接收对应的 prop 并触发 update:propName 事件。

Q4:v-model 和 ref 的区别?

对比 v-model ref
绑定方式 双向绑定 单向引用
数据流向 数据 ↔ 视图 只获取 DOM 或组件实例
使用场景 表单输入、组件通信 获取 DOM 元素、调用子组件方法

八、快速记忆

复制代码
v-model 是语法糖
props + emit 是本质

表单元素上:
:value + @input

自定义组件上:
:modelValue + @update:modelValue

多个绑定用冒号:
v-model:firstName + v-model:lastName

修饰符三兄弟:
.lazy(失焦触发)
.number(转数字)
.trim(去空格)
相关推荐
He少年2 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
史迪仔01122 小时前
[QML] QML IMage图像处理
开发语言·前端·javascript·c++·qt
AI_Claude_code2 小时前
ZLibrary访问困境方案四:利用Cloudflare Workers等边缘计算实现访问
javascript·人工智能·爬虫·python·网络爬虫·边缘计算·爬山算法
AwesomeCPA2 小时前
Miaoduo MCP 使用指南(VDI内网环境)
前端·ui·ai编程
前端大波2 小时前
前端面试通关包(2026版,完整版)
前端·面试·职场和发展
qq_433502182 小时前
Codex cli 飞书文档创建进阶实用命令 + Skill 创建&使用 小白完整教程
java·前端·飞书
IT_陈寒3 小时前
为什么我的Vite热更新老是重新加载整个页面?
前端·人工智能·后端
2301_822703203 小时前
Flutter 框架跨平台鸿蒙开发 - 创意声音合成器应用
算法·flutter·华为·harmonyos·鸿蒙