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

Vue3里v model和computed怎么配合用?这些场景和细节要注意!

terry 2小时前 阅读数 24 #SEO
文章标签 model computed

做Vue3项目时,你有没有碰到过这种情况?想给v - model绑定的值加个格式化逻辑,比如输入手机号自动分段,或者提交前把用户输入转成大写?这时候computed和v - model结合着用,能省不少事儿!但刚上手时,说不定会踩坑——比如双向绑定突然失效,或者值来回跳,今天就唠唠Vue3里v - model和computed咋配合,从原理到场景、代码实操再到避坑,一次讲透~

先搞懂:Vue3 v - model和computed各自咋运作?

先拆v - model,Vue3里,组件上的v - model默认是modelValue props + update:modelValue事件的语法糖,比如父组件写<MyComponent v - model="parentValue" />,相当于:

<MyComponent 
  :modelValue="parentValue" 
  @update:modelValue="newVal => parentValue = newVal" 
/>

再看computed,它是带getter和setter的响应式属性,普通写法const fullName = computed(() => ...)只有getter,要是想双向绑定,得写成:

const fullName = computed({
  get() { return ... },
  set(newVal) { ... }
})

那两者咋勾搭上的?举个简单例子:子组件里接收父组件的v - model值(modelValue),想用computed处理这个值的显示和修改,比如父组件传个原始字符串,子组件里用computed做格式化,输入框绑定这个computed,同时触发update事件,这时候computed的getter返回格式化后的值,setter把用户输入的新值处理成原始格式,再emit出去更新父组件数据。

啥场景下非得用computed配v - model?

场景太多了,挑几个常见的唠:

输入值格式化(显示和存储分离)

比如做个手机号输入框,用户输入时自动加空格(138 1234 5678),但实际存储的是纯数字(13812345678),这时候用computed包一层v - model的值:

<template>
  <input v - model="formattedPhone" />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const formattedPhone = computed({
  get() {
    // 把modelValue(纯数字)转成带空格的格式
    return props.modelValue.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')
  },
  set(newVal) {
    // 把用户输入的带空格字符串转成纯数字,emit回去更新父组件
    const purePhone = newVal.replace(/\s/g, '')
    emit('update:modelValue', purePhone)
  }
})
</script>

父组件用的时候就像<PhoneInput v - model="parentPhone" />,既满足用户输入体验,又保证存储值干净。

封装复杂双向绑定逻辑

比如表单里有个“开关 + 输入框”的联动:开关打开时输入框可用,关闭时清空并禁用;同时输入框有值时开关自动打开,这时候用computed把v - model和多个状态绑一起,逻辑更集中:

<template>
  <switch v - model="isEnabled" />
  <input v - model="inputVal" :disabled="!isEnabled" />
</template>
<script setup>
import { computed, ref } from 'vue'
const isEnabled = ref(false)
const rawInput = ref('')
const inputVal = computed({
  get() {
    return isEnabled.value ? rawInput.value : ''
  },
  set(newVal) {
    rawInput.value = newVal
    // 输入有值时自动打开开关
    isEnabled.value = newVal.trim().length > 0
  }
})
</script>

这里inputVal的computed同时管了isEnabled和rawInput,不用在模板里写一堆v - if或者事件监听,代码清爽多了。

权限控制下的“假”双向绑定

比如某些角色只能看不能改,这时候v - model得变成“单向”,用computed的setter里加权限判断:

const editableValue = computed({
  get() { return props.modelValue },
  set(newVal) {
    if (userHasPermission.value) {
      emit('update:modelValue', newVal)
    }
    // 没权限就不emit,保持原值
  }
})

模板里绑<input v - model="editableValue" />,用户感知不到权限逻辑,代码也不用到处写v - if控制disabled。

实操代码咋写?分“自定义组件”和“单组件内部”两种情况

情况1:自定义组件中,用computed处理父组件的v - model

步骤:

  1. 子组件用defineProps接收modelValuedefineEmits声明update:modelValue
  2. 定义computed,getter读props.modelValue并处理,setter把新值处理后emit
  3. 模板里的输入框绑这个computed。

完整例子(带格式化的输入框组件):

<!-- 子组件 FormattedInput.vue -->
<template>
  <input 
    type="text" 
    v - model="formattedValue" 
    placeholder="请输入内容"
  />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
const emit = defineEmits(['update:modelValue'])
// 用computed处理显示和提交的格式
const formattedValue = computed({
  get() {
    // 假设需求是首字母大写
    return props.modelValue.charAt(0).toUpperCase() + props.modelValue.slice(1)
  },
  set(newVal) {
    // 提交时转成全小写
    const lowerCaseVal = newVal.toLowerCase()
    emit('update:modelValue', lowerCaseVal)
  }
})
</script>

父组件用的时候:

<template>
  <FormattedInput v - model="parentValue" />
  <p>父组件的值:{{ parentValue }}</p>
</template>
<script setup>
import { ref } from 'vue'
import FormattedInput from './FormattedInput.vue'
const parentValue = ref('hello')
</script>

输入时用户看到首字母大写,父组件拿到的是全小写,完美分离显示和存储逻辑。

情况2:单组件内部,对自身v - model(或响应式数据)用computed包装

比如页面里有个搜索框,输入时实时过滤列表,同时要保留原始输入和过滤逻辑:

<template>
  <input v - model="filteredSearch" placeholder="搜索" />
  <ul>
    <li v - for="item in filteredList" :key="item.id">{{ item.name }}</li>
  </ul>
</template>
<script setup>
import { ref, computed } from 'vue'
const rawSearch = ref('') // 原始输入
const list = ref([/* 一堆数据 */])
// computed同时处理搜索输入和列表过滤
const filteredSearch = computed({
  get() {
    return rawSearch.value
  },
  set(newVal) {
    rawSearch.value = newVal
    // 这里可以加防抖、节流,或者复杂过滤逻辑
  }
})
const filteredList = computed(() => {
  return list.value.filter(item => 
    item.name.includes(rawSearch.value)
  )
})
</script>

这里filteredSearch的computed负责管理rawSearch的读写,以后要加输入防抖(比如set里用setTimeout),直接改computed的setter就行,不用动模板。

这些坑踩过没?避坑指南拿走

坑1:computed的setter漏写,导致双向绑定失效

比如想让v - model能改值,但只写了getter:

// 错误写法!只有getter,set的时候没反应
const wrongComputed = computed(() => props.modelValue)
// 模板绑v - model="wrongComputed",用户输入后值不会变

解决:必须写setter,哪怕setter里啥逻辑都没有,也得把新值emit出去:

const correctComputed = computed({
  get() { return props.modelValue },
  set(newVal) { emit('update:modelValue', newVal) }
})

坑2:值转换时触发循环更新

比如格式化逻辑写反了,getter和setter互相触发更新:

const badFormat = computed({
  get() {
    return props.modelValue.replace(/\s/g, '') // 显示纯数字
  },
  set(newVal) {
    // 错误:把纯数字又转成带空格,导致父组件更新后,getter又去掉空格,无限循环
    const formatted = newVal.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')
    emit('update:modelValue', formatted)
  }
})

解决:明确显示和存储的格式分工,getter负责“显示给用户看的格式”,setter负责“转成存储的格式”,两者逻辑不冲突:

const goodFormat = computed({
  get() {
    // 存储的是纯数字,显示时加空格
    return props.modelValue.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')
  },
  set(newVal) {
    // 用户输入的带空格,转成纯数字存起来
    const pure = newVal.replace(/\s/g, '')
    emit('update:modelValue', pure)
  }
})

坑3:多v - model时命名混乱

Vue3支持多个v - model,比如<MyComponent v - model:foo="a" v - model:bar="b" />,这时候子组件要接收foobar两个props,emitupdate:fooupdate:bar,如果用computed处理多个model,得注意命名:

<template>
  <input v - model="fooComputed" />
  <input v - model="barComputed" />
</template>
<script setup>
const props = defineProps(['foo', 'bar'])
const emit = defineEmits(['update:foo', 'update:bar'])
const fooComputed = computed({
  get() { return props.foo },
  set(newVal) { emit('update:foo', newVal) }
})
const barComputed = computed({
  get() { return props.bar },
  set(newVal) { emit('update:bar', newVal) }
})
</script>

注意:每个v - model对应的props和emit事件名要一致,computed里也得对应好,别搞混。

坑4:computed依赖的响应式数据没正确跟踪

比如在setup里用了普通变量,不是ref或reactive,computed拿不到更新:

// 错误:rawVal不是响应式的,computed不会更新
let rawVal = '' 
const wrongComputed = computed({
  get() { return rawVal },
  set(newVal) { rawVal = newVal }
})

解决:用ref包装响应式数据:

const rawVal = ref('')
const correctComputed = computed({
  get() { return rawVal.value },
  set(newVal) { rawVal.value = newVal }
})

Vue3对比Vue2,v - model + computed有啥新玩法?

Vue2里,组件上的v - model是value props + input事件,而且一个组件只能有一个v - model,Vue3做了这些升级,和computed结合更灵活:

多v - model支持,computed可分别处理

Vue3允许组件上写多个v - model:xxx,比如表单组件里同时双向绑定“姓名”和“邮箱”:

<FormSection 
  v - model:name="userName" 
  v - model:email="userEmail" 
/>

子组件里用computed分别处理name和email的双向绑定,逻辑更模块化:

<template>
  <input v - model="nameComputed" placeholder="姓名" />
  <input v - model="emailComputed" placeholder="邮箱" />
</template>
<script setup>
const props = defineProps(['name', 'email'])
const emit = defineEmits(['update:name', 'update:email'])
const nameComputed = computed({
  get() { return props.name },
  set(newVal) { emit('update:name', newVal) }
})
const emailComputed = computed({
  get() { return props.email },
  set(newVal) { emit('update:email', newVal) }
})
</script>

比Vue2里手动传多个props和事件清爽太多。

script setup语法糖,让代码更简洁

Vue3的<script setup>里,definePropsdefineEmits不用return,直接用,配合computed写起来像“原生变量”:

<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const syncValue = computed({
  get() { return props.modelValue },
  set(newVal) { emit('update:modelValue', newVal) }
})
</script>
<template>
  <input v - model="syncValue" />
</template>

对比Vue2的选项式API,少了很多this.xxx,逻辑更聚焦。

组合式API下,computed和响应式数据结合更自由

在setup里,ref/reactive和computed的组合更灵活,比如用reactive包对象,computed处理其中一个属性的双向绑定:

const formState = reactive({
  raw: '',
  formatted: ''
})
const syncFormatted = computed({
  get() { return formState.formatted },
  set(newVal) { 
    formState.formatted = newVal 
    formState.raw = newVal.toLowerCase() // 同时更新raw
  }
})

这种“局部状态 + computed联动”的写法,在Vue2里得用watch或者methods绕一圈,Vue3里更直观。

掌握这几点,v - model + computed玩得转!

其实核心逻辑就一层:v - model负责双向绑定的“通道”,computed负责这个通道里“值的加工”,不管是自定义组件传值,还是单组件内部处理表单,只要涉及“显示值≠存储值”“绑定值需要额外逻辑(格式化、权限、联动)”,就可以把computed往v - model上套。

记住这几个关键:

  • 组件上的v - model是modelValue + update:modelValue(多v - model时是xxx + update:xxx);
  • computed要写全getter和setter,setter里处理完值要emit更新;
  • 避免循环更新,明确显示和存储的格式分工;
  • 多v - model时props和emit事件名要对应。

现在再遇到需要给v - model加逻辑的场景,就知道咋用computed优雅解决啦~ 要是还有疑问,评论区甩例子,咱们再唠!

版权声明

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

热门