1. 为什么需要 computed?
在 Vue2 中,v-model 直接绑定复杂嵌套对象的深层属性时,往往会遇到几个令人头疼的痛点:

- 嵌套层级过深,模板代码冗长且难以维护
- 父子组件通信时需手动
$emit,业务逻辑分散各处 - 频繁需要数据格式转换(时间戳与日期、分与元、字符串与数组等)
而 computed + get/set 正是解决这类问题的核心利器。它相当于模板与数据源之间的一层智能适配器,既能将复杂的数据结构“展平”,又能自动完成格式转换的脏活累活。
复制代码computed: {
fullName: {
get() { return this.firstName + ' ' + this.lastName },
set(val) {
const [first, last] = val.split(' ')
this.firstName = first
this.lastName = last
}
}
}
2. 核心原理:computed 的 get/set
复制代码v-model 绑定 computed 属性
│
├── 读取时 → get() 返回处理后的值
└── 写入时 → set(newVal) 拆解并更新源数据
复制代码┌──────────┐ get ┌──────────────┐
│ Template │ ◄────────────── │ computed │
│ v-model │ ──────────────► │ (get/set) │
└──────────┘ set └──────┬───────┘
│ 读/写
▼
┌──────────────┐
│ data() │
│ (源数据结构) │
└──────────────┘
核心思路其实只有三句话:
get负责从源数据计算出展示时需要的值。set负责把用户的输入“解析”回源数据,保持原始结构不变。- 源数据最好保持规范的结构,模板只接触计算后的轻量值。
这样一来,模板代码可以做到极致简洁,而所有“脏活”都集中到了 computed 的 set 里。
3. 常见场景与技巧
3.1 对象属性映射
场景:后端返回的字段名与表单所需的命名规范不一致,无法直接统一。
复制代码// 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 }
}
}
复制代码
模板看起来清爽多了,这就是 computed 的优雅之处。
3.2 嵌套对象展平
场景:数据结构多层嵌套,模板中写 v-model="form.user.profile.name" 显得冗长。
复制代码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 }
}
}
复制代码
这种方式特别适合后端 API 返回的复杂嵌套结构,前端组件只管绑定简洁的字段名,背后的逻辑全封装在 computed 里。
3.3 数据格式转换
场景:接口使用时间戳,表单需要日期字符串;后端以“分”为单位,用户习惯按“元”输入。
复制代码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) }
}
}
复制代码
这一招在对接第三方 API 时尤为实用——后端不配合?没关系,computed 帮你兜底。
3.4 数组的增删改
场景:复选框组、标签输入、列表编辑等,经常需要处理数组状态。
复制代码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
}
}
复制代码
全选
全选/反选的逻辑用一个 computed 属性即可搞定,无需在 methods 中手动同步状态。
3.5 父子组件通信
场景:子组件接收 prop,但需要通过 v-model 实现双向绑定到父组件。
复制代码// 子组件 FormItem.vue
export default {
props: {
value: { type: String, default: '' } // v-model 默认 prop
},
computed: {
innerValue: {
get() { return this.value },
set(val) { this.$emit('input', val) } // 通知父组件
}
}
}
复制代码
子组件内部用 computed 做中转,既保证了 prop 的不可变性(单向数据流),又实现了 v-model 的双向绑定效果。
4. 完整实战示例:多层级表单
将上述技巧组合起来,便是一个完整的多层级表单实战示例。核心思路不变:源数据结构保持规范、computed 做适配层、set 中做数据清洗。
复制代码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))
}
}
}
复制代码
摘要: {{ summary }}
提交时直接拿 formData 发送给后端,无需再做任何拼装或转换,这就是 computed 适配层带来的便利。
5. computed vs methods vs watch 对比
| 维度 | computed | methods | watch |
|---|---|---|---|
| 缓存 | 有缓存,依赖不变不重算 | 每次调用都执行 | 每次变化都触发 |
| 双向绑定 | 支持 get/set 配合 v-model | 无法配合 v-model | 只监听不返回值 |
| 适用场景 | 数据转换/展平/派生 | 事件处理/副作用 | 异步操作/深监听 |
| 代码量 | 中等,但模板最简洁 | 多,模板需调方法 | 最多,需额外变量 |
| 性能 | 最优(有缓存) | 一般(无缓存) | 一般 |
通过对比可以看出,computed 天生为“数据转换”和“双向绑定”场景而生,是 Vue2 表单处理的首选工具。
6. 注意事项与最佳实践
️ Vue2 的坑
-
不要在 set 中直接修改计算属性自身
复制代码// 错误:无限递归 computed: { val: { get() { return this._val }, set(v) { this.val = v } // 死循环! } } -
Vue2 无法检测对象新增属性
复制代码// 新增属性无响应 this.formData.newField = 'hello'// 改用 $set this.$set(this.formData, 'newField', 'hello') -
computed 中的 set 必须更新源数据
复制代码// set 中不做任何更新,v-model 写入无效 computed: { val: { get() { return this.data.x }, set(v) { /* 什么都不做 */ } } }
最佳实践
| 实践 | 说明 |
|---|---|
| 源数据保持规范结构 | 嵌套对象/数组保持后端接口格式 |
| computed 做适配层 | 负责格式转换、字段映射、展平 |
| set 中做数据清洗 | 类型转换、去空格、校验等 |
| 只读 computed 做派生 | 如摘要、合计、是否禁用等 |
| 初始化写全嵌套字段 | 避免后续 $set 麻烦 |
| 大量字段用 mapState/vuex | 状态管理库更适合跨组件场景 |
最后想强调的是:computed 的 set 并非摆设,用得好可以极大简化表单相关的代码逻辑。但也要注意,不要为了使用而滥用——如果只是简单的一层数据绑定,直接用 data 里的字段即可。computed 最适用的场景,正是那些“数据结构不一致”或“需要中间处理”的场景。
