普通视图

发现新文章,点击刷新页面。
昨天以前首页

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

2025年8月11日 19:15

引言

在现代前端框架中,数据驱动视图已经是标配。理论上,数据一变,UI 就应该自动更新。
然而,实际开发中,不论是 Vue、React 还是 Angular,都可能出现“数据改了但界面没动”的情况。

这并不是框架失灵,而是我们踩中了各自的机制限制。本文将系统梳理这些“视图不更新”的真相,并给出可落地的解决方案。

一、Vue 2:defineProperty 时代的老毛病

Vue 2 使用 Object.defineProperty 劫持数据,初始化时只会追踪已有属性。

常见不更新场景:

  1. 对象新增 / 删除属性

    this.obj.newKey = 1; // ❌ 不更新
    delete this.obj.x;   // ❌ 不更新
    this.$set(this.obj, 'newKey', 1); // ✅
    
  2. 数组按索引改值 / 改 length

    this.arr[1] = 'x'; // ❌
    this.$set(this.arr, 1, 'x'); // ✅
    this.arr.splice(1, 1, 'x'); // ✅
    
  3. 深层变更没被 watch 到

    watch(obj, fn); // 默认浅监听
    watch(() => obj.a.b, fn); // ✅ 精确监听
    watch(obj, fn, { deep: true }); // ✅ 深监听
    
  4. v-for 用 index 做 key

    • DOM 复用导致错位,必须用业务唯一 ID 做 :key
  5. 更新已发生但还没渲染

    • 需要 await this.$nextTick() 再读取 DOM
  6. keep-alive 缓存

    • 切换路由/标签页时需要 :key 触发刷新
  7. 响应式对象被替换成非响应式

    • 比如 Object.freeze 的对象、原型属性
  8. Class 实例 / 原型链字段

    • Vue 无法追踪原型链上的变更
  9. Object.assign 新增属性不更新

    • 在 Vue 2 中,如果 Object.assign 往一个响应式对象里新增了之前不存在的属性,这些新属性不会被 Object.defineProperty 转成 getter/setter,所以视图不更新。
    • 修改已有属性是可以更新的,因为 getter/setter 已经存在。

示例

// ❌ 新增属性不更新
Object.assign(this.obj, { newKey: 'value' });

// ✅ 更新已有属性可以
Object.assign(this.obj, { existKey: 'newValue' });

// ✅ 新增属性用 $set
this.$set(this.obj, 'newKey', 'value');

// ✅ 或直接替换引用
this.obj = { ...this.obj, newKey: 'value' };

补充说明

  • 这个问题在 Vue 3 已解决,因为 Proxy 能动态拦截属性的新增/删除。

二、Vue 3:Proxy 时代的新坑

Vue 3 用 Proxy 解决了新增属性/数组索引不更新的问题,但也有一些“看似更新”的陷阱。

常见不更新场景:

  1. 解构导致丢失响应性

    const { a } = reactiveObj; // ❌
    const { a } = toRefs(reactiveObj); // ✅
    
  2. ref 在 JS 中没 .value

    count++; // ❌
    count.value++; // ✅
    
  3. shallowReactive / shallowRef 只追踪浅层

    • 深层改值需 triggerRef
  4. 直接修改 props

    • 必须通过 emit('update:xxx') 或本地副本
  5. watch 依赖没写对

    • 默认浅监听,需 watch getter 或 { deep: true }
  6. key 复用 / keep-alive 同 Vue 2

  7. 对 markRaw / readonly 对象改值

    • 本来就不会触发
  8. 异步更新批处理

    • 改很多次值,DOM 只会最后更新一次,需要立即读取 DOM 用 await nextTick()

三、React:引用没变就不渲染

React 的渲染依赖 state 引用变化,而不是深比较。

常见不更新场景:

  1. 直接改 state 而不 setState

    this.state.count++; // ❌
    this.setState({ count: this.state.count + 1 }); // ✅
    
  2. 深层对象/数组直接改值

    user.name = 'B';
    setUser(user); // ❌ 引用没变
    setUser({ ...user, name: 'B' }); // ✅
    
  3. PureComponent / React.memo 下引用没变

    • 浅比较返回 true → 不渲染
  4. 闭包陷阱

    setTimeout(() => setCount(count + 1), 1000); // ❌ count 旧值
    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()

五、三大框架的共性坑

  1. 列表 key 复用导致错位
  2. 数据源不是响应式(冻结对象、原型链字段)
  3. DOM 读取时机错误(改数据后立即读 DOM)
  4. 组件缓存(keep-alive / memo / OnPush)

六、如何避免“改了不更新”

  • 理解框架的响应式原理
  • 遵循框架提供的状态修改 API
  • 对对象/数组改值时,优先用新引用
  • 必要时用强制刷新(Vue $forceUpdate / React forceUpdate / Angular detectChanges
  • 列表渲染一定要用稳定唯一的 key

七、总结

  • Vue 2:最怕新增属性、数组索引 → 用 $set 或替换引用
  • Vue 3:解构丢响应、ref.value、浅响应等细节
  • React:引用必须变化
  • Angular:变更检测必须跑

理解了这些“真相”,你就能在第一时间判断出问题出在哪,并用最短的时间修复它。

❌
❌