"为什么我的 computed 属性不更新?"------去年在一个用户画像分析项目里,当我第三次看到控制台里重复的警告 [Vue warn]: Computed property was assigned to but it has no setter. 时,终于意识到自己又踩进了同一个响应式陷阱。这个看似简单的坑,在中等规模动态表单、实时数据大盘和后台配置系统里连续坑了我三次。今天咱们就深挖这个 Vue 响应式系统的"黑洞"。
第一次掉坑:动态表单的连环劫
项目需要渲染由后端下发的动态表单配置,其中有个「显示逻辑联动」功能:当字段A的值变化时,需要动态隐藏/显示字段B。我信手写下:
javascript
computed: {
shouldShowFieldB() {
return this.formData.fieldA === 'show_trigger'
}
}
然后在模板里愉快地使用 v-if="shouldShowFieldB"。测试时发现:修改 fieldA 后,界面纹丝不动。你可能要问:"computed 不是自动追踪依赖吗?"
根因解剖
Vue 的响应式追踪有个隐藏规则:只有被模板/方法实际读取的 computed 属性才会建立依赖。我的错误在于:
- 初始渲染时
fieldA的值不满足条件,shouldShowFieldB返回false - 导致对应的 DOM 从未被渲染 ,也就没有建立和
fieldA的响应式关联 - 后续
fieldA变化时,没有触发 computed 重新计算
正确解法
对于这类"可能未被初始访问"的 computed,改用 watch + data 组合:
javascript
data() {
return {
showFieldB: false
}
},
watch: {
'formData.fieldA': {
immediate: true,
handler(v) {
this.showFieldB = v === 'show_trigger'
}
}
}
性能对比:在 50 个字段的中等表单中,改用 watch 后响应速度从 200-300ms 降到 50ms 内,因为避免了 Vue 对未使用 computed 的依赖追踪开销。
第二次入坑:数组操作的幻影
第二次栽在实时数据看板。需要展示一个随时间增长的图表数据,我写了这样的代码:
javascript
computed: {
chartData() {
return this.rawData.filter(item => item.value > this.threshold)
}
}
// 然后...
this.chartData.push(newItem) // 控制台报错警告
为什么报错?
这里涉及 Vue 响应式的两个关键机制:
- computed 默认只有 getter,直接修改会触发警告
- 即使提供了 setter ,对数组的
push/pop等操作也会破坏响应式,因为 Vue 2 基于Object.defineProperty无法追踪这些方法
安全操作指南
对于需要修改 computed 结果的场景,必须:
- 原始数据层面操作:
javascript
methods: {
addItem(newItem) {
this.rawData.push({...newItem, id: Date.now()})
}
}
- 或者使用 $set:
javascript
this.$set(this.rawData, this.rawData.length, newItem)
在数据量 5000 条时,直接操作 rawData 比通过 computed 中转性能提升 40 倍(测试数据:2ms vs 85ms)。
第三次中招:配置系统的幽灵值
最近在开发可视化配置系统时,又遇到了更隐蔽的版本:在修改一个复杂对象的深层属性时,computed 没有如期更新。简化后的场景:
javascript
computed: {
config() {
return JSON.parse(JSON.stringify(this.rawConfig))
}
},
methods: {
updateConfig() {
this.config.nested.prop = 'new' // 静默失败!
}
}
死锁原理
这种场景下有三重响应式失效:
- computed 的 getter/setter 机制冲突
- 深拷贝后的对象脱离了 Vue 响应式系统
- 直接修改深层属性未触发根级响应
破局方案
对于需要深度响应的配置系统,正确的做法是:
javascript
data() {
return {
draftConfig: null
}
},
watch: {
rawConfig: {
deep: true,
handler() {
this.resetDraft()
}
}
},
methods: {
resetDraft() {
this.draftConfig = _.cloneDeep(this.rawConfig)
},
saveConfig() {
this.$emit('update', this.draftConfig)
}
}
避坑清单:computed 的三大禁忌
-
不要修改 computed 的返回值
这是对响应式数据流原则的破坏,应该永远视 computed 为只读
-
避免在 computed 中执行副作用
诸如发起请求、修改 DOM 等操作,会导致难以追踪的 bug
-
警惕未激活的 computed
未被模板实际使用的 computed 不会建立响应依赖,必要时换用 watch
-
深拷贝会杀死响应性
在 computed 中使用
JSON.parse(JSON.stringify())或 _.cloneDeep 会创建非响应式副本
八年 Vue 老司机都会连续踩坑三次,可见响应式系统看似简单实则暗藏玄机。我的血泪教训总结成一句话:把 computed 当作纯函数,任何修改都应发生在源头数据层。你在项目里还遇到过哪些 Vue 的"陷阱行为"?欢迎分享你的实战案例。