为什么数据变了界面却不动?——Vue / React / Angular 常见渲染“失效”场景全解析

311 阅读3分钟

引言

在现代前端框架中,数据驱动视图已经是标配。理论上,数据一变,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。