引言
在现代前端框架中,数据驱动视图已经是标配。理论上,数据一变,UI 就应该自动更新。
然而,实际开发中,不论是 Vue、React 还是 Angular,都可能出现“数据改了但界面没动”的情况。
这并不是框架失灵,而是我们踩中了各自的机制限制。本文将基于具体 API 和常见写法,梳理这些“渲染失效”的真相,并给出对应的解决方案。
一、Vue 2:defineProperty 时代的 API 限制
Vue 2 用 Object.defineProperty 劫持属性,初始化时只追踪已有的 key,这导致多个 API 或写法在运行时新增数据时不会触发视图更新。
1. obj.newKey = value / delete obj.key
- 问题:新增或删除属性不会被劫持
- 解决:
this.$set(obj, 'newKey', value)/this.$delete(obj, 'key')
2. arr[index] = value / arr.length = n
- 问题:按索引修改或直接改长度不会触发
- 解决:
this.$set(arr, index, value)或arr.splice(index, 1, value)
3. watch(obj, fn) 忘记 { deep: true }
- 问题:默认浅监听,深层属性变化不触发
- 解决:
watch(obj, fn, { deep: true })或watch(() => obj.a.b, fn)
4. v-for 使用 index 作为 key
- 问题:DOM 复用导致渲染错位
- 解决:用唯一业务 ID 作为
:key
5. DOM 读取时机错误
- 问题:改数据后立即读 DOM,拿到旧值
- 解决:
await this.$nextTick()
6. keep-alive 缓存
- 问题:组件切换后数据不刷新
- 解决:为组件加
:key或调整<keep-alive>策略
7. Object.freeze(obj)
- 问题:冻结对象不可响应
8. Class 实例属性 / 原型链属性
- 问题:原型链上的属性 Vue 无法追踪
9. Object.assign 新增属性
-
问题:新增属性不会被转成 getter/setter
-
解决:
// ❌ 不更新 Object.assign(this.obj, { newKey: 'v' }); // ✅ 用 $set this.$set(this.obj, 'newKey', 'v'); // ✅ 或替换引用 this.obj = { ...this.obj, newKey: 'v' };
二、Vue 3:Proxy 时代的新陷阱
Vue 3 用 Proxy 解决了新增属性、数组索引不响应的问题,但还有一些 API/写法依然可能导致“不更新”。
1. 解构丢失响应性
const { a } = reactiveObj; // ❌
const { a } = toRefs(reactiveObj); // ✅
2. ref 在 JS 中忘记 .value
count++; // ❌
count.value++; // ✅
3. shallowReactive / shallowRef
- 问题:只追踪第一层,深层改值不会更新
- 解决:用
reactive或手动triggerRef
4. 直接改 props
- 问题:props 是只读的,改了不生效
- 解决:用
emit('update:xxx')或本地副本
5. watch(obj, fn) 忘记写 getter
- 问题:默认浅监听,深层属性需 watch getter 或
{ deep: true }
6. key 复用 / keep-alive
- 同 Vue 2
7. markRaw / readonly 对象改值
- 本来就不触发更新
8. 异步批处理更新
- 问题:多次改值只会批量更新一次
- 解决:立即读 DOM 用
await nextTick()
三、React:引用没变就不渲染
React 渲染依赖 state/props 引用变化,深层改值但引用不变不会更新。
1. 直接改 state
this.state.count++; // ❌
this.setState({ count: this.state.count + 1 }); // ✅
2. 直接改对象/数组内部
user.name = 'B';
setUser(user); // ❌ 引用没变
setUser({ ...user, name: 'B' }); // ✅
3. PureComponent / React.memo
- 问题:浅比较相等 → 跳过渲染
4. 闭包陷阱
setTimeout(() => setCount(count + 1), 1000); // ❌
setTimeout(() => setCount(c => c + 1), 1000); // ✅
5. shouldComponentUpdate 返回 false
- 手动阻止了更新
6. Context Provider 的 value 引用没变
- 必须传新对象/引用
四、Angular:变更检测没跑
Angular 使用 Zone.js 捕获异步事件驱动变更检测。
1. OnPush 策略下引用没变
this.user.name = 'B'; // ❌
this.user = { ...this.user, name: 'B' }; // ✅
2. Zone 外改值
- 必须用
this.zone.run(() => { ... })
3. 第三方回调不触发检测
- 手动调用
ChangeDetectorRef.detectChanges()
五、三大框架的共性坑
- 列表
key复用导致错位 - 数据源不是响应式对象(冻结、原型链)
- DOM 读取时机错误(需 nextTick/forceUpdate/detectChanges)
- 组件缓存(keep-alive / memo / OnPush)
六、避免“改了不更新”的通用建议
- 理解各框架的响应式原理
- 用框架推荐的 API 修改数据
- 对对象/数组,优先用新引用替换
- 必要时强制刷新
- 列表渲染用稳定唯一 key
七、总结
- Vue 2:新增属性、数组索引 →
$set或替换引用 - Vue 3:解构丢响应、浅响应、ref.value
- React:引用必须变化
- Angular:变更检测必须跑
理解每个 API 的触发条件和限制,才能写出既高效又稳定的响应式 UI。