1. 为什么需要 computed?
在 Vue2 中,v-model 直接绑定复杂嵌套对象的深层属性时,会遇到以下痛点:
- 嵌套层级深,模板代码冗长难读
- 父子组件通信时需要手动
$emit,逻辑分散 - 需要对数据做格式转换(时间戳↔日期、分↔元、字符串↔数组等)
computed + get/set 是解决这类问题的核心利器。
js
computed: {
fullName: {
get() { return this.firstName + ' ' + this.lastName },
set(val) {
const [first, last] = val.split(' ')
this.firstName = first
this.lastName = last
}
}
}
模板中
<input v-model="fullName" />即可双向绑定,数据和展示格式完全解耦。
2. 核心原理:computed 的 get/set
scss
v-model 绑定 computed 属性
│
├── 读取时 → get() 返回处理后的值
└── 写入时 → set(newVal) 拆解并更新源数据
kotlin
┌──────────┐ get ┌──────────────┐
│ Template │ ◄────────────── │ computed │
│ v-model │ ──────────────► │ (get/set) │
└──────────┘ set └──────┬───────┘
│ 读/写
▼
┌──────────────┐
│ data() │
│ (源数据结构) │
└──────────────┘
关键原则:
get负责从源数据计算出展示值set负责把用户输入写回源数据- 源数据保持规范结构,模板只接触计算后的值
3. 常见场景与技巧
3.1 对象属性映射
场景:后端返回的接口字段名和表单字段名不一致。
js
// data 中的源数据
data() {
return {
form: {
user_name: '', // 后端字段
user_email: '' // 后端字段
}
}
},
computed: {
// 表单绑定用的字段名
name: {
get() { return this.form.user_name },
set(val) { this.form.user_name = val }
},
email: {
get() { return this.form.user_email },
set(val) { this.form.user_email = val }
}
}
vue
<template>
<!-- 模板简洁干净 -->
<input v-model="name" placeholder="姓名" />
<input v-model="email" placeholder="邮箱" />
</template>
3.2 嵌套对象展平
场景 :数据是多层嵌套结构,直接在模板中 v-model="form.user.profile.name" 太冗长。
js
data() {
return {
form: {
user: {
profile: {
name: '',
age: 0,
gender: ''
}
},
meta: {
tags: [],
createTime: 0
}
}
}
},
computed: {
name: {
get() { return this.form.user.profile.name },
set(val) { this.form.user.profile.name = val }
},
age: {
get() { return this.form.user.profile.age },
set(val) { this.form.user.profile.age = Number(val) }
},
gender: {
get() { return this.form.user.profile.gender },
set(val) { this.form.user.profile.gender = val }
}
}
vue
<template>
<input v-model="name" />
<input v-model.number="age" />
<select v-model="gender">
<option value="">请选择</option>
<option value="male">男</option>
<option value="female">女</option>
</select>
</template>
3.3 数据格式转换
场景:接口用时间戳,表单用日期字符串;接口用分,表单用元。
js
data() {
return {
form: {
createTime: 1700000000, // 时间戳(秒)
price: 9999 // 分
}
}
},
computed: {
// 时间戳 ↔ 日期字符串
createTimeStr: {
get() {
const d = new Date(this.form.createTime * 1000)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
},
set(val) {
this.form.createTime = Math.floor(new Date(val).getTime() / 1000)
}
},
// 分 ↔ 元
priceYuan: {
get() { return (this.form.price / 100).toFixed(2) },
set(val) { this.form.price = Math.round(parseFloat(val) * 100) }
},
// 数组 ↔ 逗号分隔字符串
tagsStr: {
get() { return this.form.meta.tags.join(', ') },
set(val) { this.form.meta.tags = val.split(',').map(s => s.trim()).filter(Boolean) }
}
}
vue
<template>
<input v-model="createTimeStr" type="date" />
<input v-model="priceYuan" /> <!-- 用户看到 99.99 -->
<input v-model="tagsStr" /> <!-- 输入: vue, js, css -->
</template>
3.4 数组的增删改
场景:复选框组、标签输入、列表编辑等场景。
js
data() {
return {
form: {
selectedFruits: [], // 已选水果
allFruits: ['apple', 'banana', 'cherry', 'date']
}
}
},
computed: {
// 复选框全选状态
allChecked: {
get() {
return this.form.allFruits.length > 0 &&
this.form.allFruits.every(f => this.form.selectedFruits.includes(f))
},
set(val) {
this.form.selectedFruits = val ? [...this.form.allFruits] : []
}
},
// 数组索引化:把数组转成可编辑的对象
fruitMap() {
const map = {}
this.form.allFruits.forEach((fruit, i) => {
map[i] = fruit
})
return map
}
}
vue
<template>
<!-- 全选复选框 -->
<input type="checkbox" v-model="allChecked" /> 全选
<!-- 列表复选框 -->
<label v-for="fruit in allFruits" :key="fruit">
<input
type="checkbox"
:value="fruit"
v-model="form.selectedFruits"
/>
{{ fruit }}
</label>
</template>
3.5 父子组件通信
场景 :子组件接收 prop,通过 v-model 双向绑定回父组件。
js
// 子组件 FormItem.vue
export default {
props: {
value: { type: String, default: '' } // v-model 默认 prop
},
computed: {
innerValue: {
get() { return this.value },
set(val) { this.$emit('input', val) } // 通知父组件
}
}
}
vue
<!-- 子组件模板 -->
<template>
<div class="form-item">
<input v-model="innerValue" />
</div>
</template>
<!-- 父组件 -->
<template>
<form-item v-model="formData.name" />
<form-item v-model="formData.email" />
</template>
4. 完整实战示例:多层级表单
js
export default {
data() {
return {
// 源数据保持嵌套结构,方便提交给后端
formData: {
id: 0,
user: {
name: '',
age: 0,
birthday: 0,
address: {
city: '',
street: ''
}
},
preferences: {
theme: 'light',
language: 'zh-CN'
},
tags: [],
price: 0
}
}
},
computed: {
/* ---- 展平嵌套字段 ---- */
name: {
get() { return this.formData.user.name },
set(v) { this.formData.user.name = v }
},
age: {
get() { return this.formData.user.age },
set(v) { this.formData.user.age = Number(v) || 0 }
},
city: {
get() { return this.formData.user.address.city },
set(v) { this.formData.user.address.city = v }
},
street: {
get() { return this.formData.user.address.street },
set(v) { this.formData.user.address.street = v }
},
/* ---- 格式转换 ---- */
birthdayStr: {
get() {
if (!this.formData.user.birthday) return ''
const d = new Date(this.formData.user.birthday * 1000)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
},
set(v) {
this.formData.user.birthday = v ? Math.floor(new Date(v).getTime() / 1000) : 0
}
},
priceYuan: {
get() { return (this.formData.price / 100).toFixed(2) },
set(v) { this.formData.price = Math.round(parseFloat(v) * 100) || 0 }
},
/* ---- 数组处理 ---- */
tagsStr: {
get() { return this.formData.tags.join(', ') },
set(v) {
this.formData.tags = v
.split(',')
.map(s => s.trim())
.filter(Boolean)
}
},
/* ---- 派生只读值 ---- */
summary() {
return `${this.name}, ${this.age}岁, ${this.city} ${this.street}`
}
},
methods: {
submit() {
// 直接提交原始嵌套结构,无需二次转换
console.log('提交数据:', JSON.stringify(this.formData, null, 2))
}
}
}
vue
<template>
<div class="form">
<input v-model="name" placeholder="姓名" />
<input v-model.number="age" placeholder="年龄" />
<input v-model="birthdayStr" type="date" />
<input v-model="city" placeholder="城市" />
<input v-model="street" placeholder="街道" />
<input v-model="tagsStr" placeholder="标签(逗号分隔)" />
<input v-model="priceYuan" placeholder="价格(元)" />
<p>摘要: {{ summary }}</p>
<button @click="submit">提交</button>
</div>
</template>
5. computed vs methods vs watch 对比
| 维度 | computed | methods | watch |
|---|---|---|---|
| 缓存 | ✅ 有缓存,依赖不变不重算 | ❌ 每次调用都执行 | ❌ 每次变化都触发 |
| 双向绑定 | ✅ 支持 get/set 配合 v-model | ❌ 无法配合 v-model | ❌ 只监听不返回值 |
| 适用场景 | 数据转换/展平/派生 | 事件处理/副作用 | 异步操作/深监听 |
| 代码量 | 中等,但模板最简洁 | 多,模板需调方法 | 最多,需额外变量 |
| 性能 | 最优(有缓存) | 一般(无缓存) | 一般 |
6. 注意事项与最佳实践
⚠️ Vue2 的坑
-
不要在 set 中直接修改计算属性自身
js// ❌ 错误:无限递归 computed: { val: { get() { return this._val }, set(v) { this.val = v } // 死循环! } } -
Vue2 无法检测对象新增属性
js// ❌ 新增属性无响应 this.formData.newField = 'hello' // ✅ 用 $set this.$set(this.formData, 'newField', 'hello') -
computed 中的 set 必须更新源数据
js// ❌ set 中不做任何更新,v-model 写入无效 computed: { val: { get() { return this.data.x }, set(v) { /* 什么都不做 */ } } }
✅ 最佳实践
| 实践 | 说明 |
|---|---|
| 源数据保持规范结构 | 嵌套对象/数组保持后端接口格式 |
| computed 做适配层 | 负责格式转换、字段映射、展平 |
| set 中做数据清洗 | 类型转换、去空格、校验等 |
| 只读 computed 做派生 | 如摘要、合计、是否禁用等 |
| 初始化写全嵌套字段 | 避免后续 $set 麻烦 |
| 大量字段用 mapState/vuex | 状态管理库更适合跨组件场景 |