引言
在现代前端框架中,数据驱动视图已经是标配。理论上,数据一变,UI 就应该自动更新。
然而,实际开发中,不论是 Vue、React 还是 Angular,都可能出现"数据改了但界面没动"的情况。
这并不是框架失灵,而是我们踩中了各自的机制限制。本文将系统梳理这些"视图不更新"的真相,并给出可落地的解决方案。
一、Vue 2:defineProperty 时代的老毛病
Vue 2 使用 Object.defineProperty
劫持数据,初始化时只会追踪已有属性。
常见不更新场景:
-
对象新增 / 删除属性
jsthis.obj.newKey = 1; // ❌ 不更新 delete this.obj.x; // ❌ 不更新 this.$set(this.obj, 'newKey', 1); // ✅
-
数组按索引改值 / 改 length
jsthis.arr[1] = 'x'; // ❌ this.$set(this.arr, 1, 'x'); // ✅ this.arr.splice(1, 1, 'x'); // ✅
-
深层变更没被 watch 到
jswatch(obj, fn); // 默认浅监听 watch(() => obj.a.b, fn); // ✅ 精确监听 watch(obj, fn, { deep: true }); // ✅ 深监听
-
v-for 用 index 做 key
- DOM 复用导致错位,必须用业务唯一 ID 做
:key
- DOM 复用导致错位,必须用业务唯一 ID 做
-
更新已发生但还没渲染
- 需要
await this.$nextTick()
再读取 DOM
- 需要
-
keep-alive 缓存
- 切换路由/标签页时需要
:key
触发刷新
- 切换路由/标签页时需要
-
响应式对象被替换成非响应式
- 比如
Object.freeze
的对象、原型属性
- 比如
-
Class 实例 / 原型链字段
- Vue 无法追踪原型链上的变更
二、Vue 3:Proxy 时代的新坑
Vue 3 用 Proxy 解决了新增属性/数组索引不更新的问题,但也有一些"看似更新"的陷阱。
常见不更新场景:
-
解构导致丢失响应性
jsconst { a } = reactiveObj; // ❌ const { a } = toRefs(reactiveObj); // ✅
-
ref 在 JS 中没
.value
jscount++; // ❌ count.value++; // ✅
-
shallowReactive / shallowRef 只追踪浅层
- 深层改值需
triggerRef
- 深层改值需
-
直接修改 props
- 必须通过
emit('update:xxx')
或本地副本
- 必须通过
-
watch 依赖没写对
- 默认浅监听,需 watch getter 或
{ deep: true }
- 默认浅监听,需 watch getter 或
-
key 复用 / keep-alive 同 Vue 2
-
对 markRaw / readonly 对象改值
- 本来就不会触发
-
异步更新批处理
- 改很多次值,DOM 只会最后更新一次,需要立即读取 DOM 用
await nextTick()
- 改很多次值,DOM 只会最后更新一次,需要立即读取 DOM 用
三、React:引用没变就不渲染
React 的渲染依赖 state 引用变化,而不是深比较。
常见不更新场景:
-
直接改 state 而不 setState
jsthis.state.count++; // ❌ this.setState({ count: this.state.count + 1 }); // ✅
-
深层对象/数组直接改值
jsuser.name = 'B'; setUser(user); // ❌ 引用没变 setUser({ ...user, name: 'B' }); // ✅
-
PureComponent / React.memo 下引用没变
- 浅比较返回 true → 不渲染
-
闭包陷阱
jssetTimeout(() => setCount(count + 1), 1000); // ❌ count 旧值 setTimeout(() => setCount(c => c + 1), 1000); // ✅
-
shouldComponentUpdate 返回 false
- 手动阻止了渲染
-
Context 没更新
- Provider value 引用没变 → 消费者不更新
四、Angular:变更检测没跑
Angular 依赖 Zone.js 触发变更检测。
常见不更新场景:
-
OnPush 策略下引用没变
jsthis.user.name = 'B'; // ❌ this.user = { ...this.user, name: 'B' }; // ✅
-
Zone 之外改值
- 需要
this.zone.run(() => { ... })
- 需要
-
第三方库回调不触发检测
- 手动调用
ChangeDetectorRef.detectChanges()
- 手动调用
五、三大框架的共性坑
- 列表 key 复用导致错位
- 数据源不是响应式(冻结对象、原型链字段)
- DOM 读取时机错误(改数据后立即读 DOM)
- 组件缓存(keep-alive / memo / OnPush)
六、如何避免"改了不更新"
- 理解框架的响应式原理
- 遵循框架提供的状态修改 API
- 对对象/数组改值时,优先用新引用
- 必要时用强制刷新(Vue
$forceUpdate
/ ReactforceUpdate
/ AngulardetectChanges
) - 列表渲染一定要用稳定唯一的 key
七、总结
- Vue 2 :最怕新增属性、数组索引 → 用
$set
或替换引用 - Vue 3:解构丢响应、ref.value、浅响应等细节
- React:引用必须变化
- Angular:变更检测必须跑
理解了这些"真相",你就能在第一时间判断出问题出在哪,并用最短的时间修复它。