4-10.【状态管理】状态管理常见的“隐性 Bug”有哪些?为什么修改对象属性 UI 不更新?为什么数组 push 后不刷新?

43 阅读3分钟

在 ArkUI 的开发中,最让开发者头疼的往往不是语法错误,而是“代码运行了,但 UI 纹理不动”。这些“隐性 Bug”大多源于对 ArkTS 状态监听机制(Proxy/Wrapper) 的理解偏差。


1. 为什么修改对象属性 UI 不更新?

这是最典型的“观察失败”场景。

根本原因:深度观察限制

@State@Prop@Link 等装饰器,默认只能观察到 第一层 变量的引用变化或基本类型的赋值。

  • 无效操作: 如果你定义了 user: User = new User('Alice'),执行 this.user.name = 'Bob'。对于框架来说,user 这个对象的**内存地址(引用)**没有变,它感知不到内部属性的微小挪动。

解决方案:

  1. 使用 @Observed + @ObjectLink 将类标记为 @Observed,并在子组件中通过 @ObjectLink 接收。这样框架会重写该类的 Setter,实现属性级监听。

  2. 重新赋值(引用触发): 强制让对象“变身”。

    TypeScript

    // 这种写法能触发更新,因为 user 的引用变了
    this.user = { ...this.user, name: 'Bob' }; 
    

2. 为什么数组 push 后不刷新?

数组的操作是状态管理的“重灾区”。

根本原因:非原地修改与监听范围

ArkUI 能够自动识别数组的某些变异方法(如 push, pop, splice, shift 等),但在以下情况下会失效:

  1. 直接修改索引: 执行 this.list[0] = newItem。这不会触发 UI 更新,因为数组的引用没变,且这种赋值操作没有被框架的拦截器捕获。
  2. 嵌套对象的数组: 如果你 push 了一个对象,后续修改这个对象的某个属性(如 this.list[0].status = true),UI 不会刷新。因为数组只管“成员的增减”,不管“成员内部的细节”。

解决方案:

  • 针对索引修改: 使用 this.list.splice(0, 1, newItem)this.list = [...this.list]
  • 针对成员属性: 必须配合 @Observed 装饰数组内的元素类,并结合 LazyForEachkeyGenerator 确保 Key 随数据变化。

3. 其他常见的“隐性 Bug”

A. 构造函数中的异步赋值

aboutToAppearonCreate 中发起异步请求,直接给 @State 变量赋值。

  • Bug 表现: 数据回来了,但组件已经渲染完毕,且由于闭包或作用域问题,某些复杂的嵌套组件没有收到更新通知。
  • 规避: 确保异步回调在主线程执行,且优先使用 AppStorageLocalStorage 这种有全局中心调度能力的状态仓库。

B. “影子状态” (Shadow State)

在父组件修改了传给子组件 @Prop 的值,子组件 UI 没变。

  • 原因: 如果子组件在内部也修改过这个 @Prop,由于 @Prop 是单向拷贝,父组件的后续同步可能会因为框架内部的 Diff 策略被“静默掉”或者产生非预期的覆盖。
  • 规避: 严格遵循“单向数据流”,子组件想改数据?发事件(Emitter)给父组件改。

C. 循环依赖引发的死循环

build() 过程中修改了某个 @State(例如在计算宽度时又 set 了宽度)。

  • Bug 表现: 应用卡死、帧率骤降、Log 报错 State changed during rendering
  • 规避: build() 函数必须是纯函数。严禁在渲染流程中写任何 Setter 逻辑。

总结:排查清单

当 UI 不刷新时,按以下顺序自查:

  1. 层级对吗? 超过一层的属性修改,用了 @Observed 吗?
  2. 引用变了吗? 数组或对象的操作是原地修改还是替换了新实例?
  3. Key 稳吗? ForEach 的 Key 是否唯一且能反映数据变化?
  4. 改对地方了吗? 是不是在非 UI 线程(如 Worker)里偷偷改了数据?