你好,我是华鸿技术栈的曹老师。
最近在写一个页面,碰到一个挺典型的状态管理问题。场景不算复杂——网络请求回来数据,更新页面上的几个字段,按理说 @State 或者 @ObservedV2 加 @Trace 应该能搞定。但偏偏卡在一个地方:数据确实变了,@LocalBuilder 里面的 UI 一动不动。
排查了一圈,发现踩的是一个挺容易忽略的细节。记下来,如果你也碰到类似的情况,这篇应该能帮你省点时间。
问题长什么样
先看一段能复现问题的代码。你用 DevEco Studio 新建一个页面,把下面的代码贴进去跑一下,就能看到效果:
typescriptimport { UIUtils } from '@kit.ArkUI'
@Entry@ComponentV2export struct Index { @Local person: Person = new Person()
aboutToAppear(): void { // 模拟网络请求返回数据 setTimeout(() => { this.person = UIUtils.makeObserved( { mine: { size: '大', color: '蓝色' } }) }, 1000) }
@LocalBuilder myBuilder(item: Bag) { Column() { Text(item.size) Text(item.color) } }
build() { Column() { this.myBuilder(this.person.mine) Button('change') .onClick(() => { let num = Math.random() * 100 this.person.mine.color = 红${num} }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) }}
@ObservedV2class Bag { @Trace size: string = '初始size小' @Trace color: string = '初始color红色';}
class Person { mine: Bag = new Bag()}运行效果:页面上显示的一直是初始值“初始size小”和“初始color红色”。aboutToAppear 里模拟的网络数据回来了,this.person 也确实被重新赋值了,但 UI 没跟着变。点 change 按钮,this.person.mine.color 被改了,UI 还是不动。
问题来了:@ObservedV2 加了,@Trace 也加了,UIUtils.makeObserved 也用了,为什么还是不刷新?
排查过程遇到这种“数据变了 UI 不动”的问题,通常从几个方向排查。咱们挨个过一遍。
第一,被监听的类有没有加 @ObservedV2?Bag 类需要被观测,代码里已经用 @ObservedV2 装饰了,里面的 size 和 color 也加了 @Trace。这一步没问题。
第二,网络数据转成可观测对象了吗?
网络请求回来的数据通常是 JSON 字符串,JSON.parse() 之后得到的是普通 JS 对象,不是通过 new 创建的类实例。这种普通对象直接赋值给状态变量,ArkTS 的状态管理机制是感知不到的——因为它没有被包装成“可观测”的数据。
代码里用了 UIUtils.makeObserved() 把字面量对象转成了可深度观测的状态变量,这一步也没问题。
第三,@LocalBuilder 的参数是怎么传进去的?
这是关键。
@LocalBuilder 的传参方式有三种:引用传递、值传递、回调传递。不同方式对状态变化的响应逻辑不一样。
看问题代码里这一行:
typescriptthis.myBuilder(this.person.mine)
这里传的是 this.person.mine,一个 Bag 类型的对象。看起来像是引用传递,但实际上,当你整体替换 this.person 时(在 aboutToAppear 里做的就是这个操作),this.person.mine 这个引用指向的还是老的那个 mine 对象,不是新的。@LocalBuilder 拿到的参数是旧引用,所以 UI 不更新。
实际就是:数据虽然变了,但传进 @LocalBuilder 的那根“绳子”还拴在旧数据上。
怎么修
两个方案,选哪个看你的具体场景。
方案一:拆开传,绕过整体替换的问题
既然整体替换 this.person 会导致 this.person.mine 的引用断掉,那就别整体传 mine 对象,把 mine 里面的属性拆开传进去:
typescriptimport { UIUtils } from '@kit.ArkUI';
@Entry@ComponentV2export struct refreshIndex { @Local person: Person = new Person();
aboutToAppear(): void { setTimeout(() => { this.person = UIUtils.makeObserved( { mine: { size: '大2', color: '蓝色2' } }); }, 1000); }
@LocalBuilder myBuilder(item: Bag) { Column() { Text(item.size); Text(item.color); }; }
build() { Column() { // 拆开传,把 person.mine 的属性作为新对象的属性传进去 this.myBuilder({ size: this.person.mine.size, color: this.person.mine.color }); Button('change') .onClick(() => { let num = Math.random() * 100; this.person.mine.color = 红${num}; }); } .width('100%') .height('100%') .justifyContent(FlexAlign.Center); }}
class Person { mine: Bag = new Bag();}
class Bag { size: string = '初始size小'; color: string = '初始color红色';}注意,这个方案里 Bag 类不需要加 @ObservedV2 和 @Trace,因为 UIUtils.makeObserved 已经帮你把整个数据转成可观测的了。你在调用 this.myBuilder 时传的是 { size: …, color: … } 这样一个新对象,它里面的值取自 this.person.mine,而 this.person 本身是被 makeObserved 处理过的可观测对象——值一变,这个新对象也会跟着变。
什么时候用方案一:你的数据结构比较简单,@LocalBuilder 只用到了对象里的几个属性,不涉及深层嵌套。
方案二:传整个对象,靠 @ObservedV2 驱动刷新
如果 @LocalBuilder 里需要用到的属性很多,一个个拆开传太啰嗦,那就直接把整个 person 传进去。这时候需要靠 @ObservedV2 和 @Trace 来驱动刷新:
typescript@Entry@ComponentV2export struct refreshIndex { @Local person: Person = new Person();
aboutToAppear(): void { setTimeout(() => { // 注意:这里是逐属性赋值,不是整体替换 this.person.mine.size = '大2'; this.person.mine.color = '蓝色2'; }, 1000); }
@LocalBuilder myBuilder(item: Person) { Column() { Text(item.mine.size); Text(item.mine.color); }; }
build() { Column() { this.myBuilder(this.person); Button('change') .onClick(() => { let num = Math.random() * 100; this.person.mine.color = 红${num}; }); } .width('100%') .height('100%') .justifyContent(FlexAlign.Center); }}
class Person { mine: Bag = new Bag();}
@ObservedV2class Bag { @Trace size: string = '初始size小'; @Trace color: string = '初始color红色';}这里有两个要点:
第一,不能整体替换 this.person。如果你写 this.person = 新对象,this.person 的引用就变了,@LocalBuilder 里面拿到的还是旧引用,UI 不动。所以网络数据回来后,要逐个属性赋值:this.person.mine.size = '大2'。
第二,Bag 类必须保留 @ObservedV2 和 @Trace。这个方案没有用 UIUtils.makeObserved,完全是靠 @ObservedV2 装饰器让 ArkTS 追踪属性变化。
属性多了写起来确实麻烦,可以封装一个方法来批量赋值:
typescriptupdateMine(size: string, color: string) { this.person.mine.size = size; this.person.mine.color = color;}什么时候用方案二:数据结构复杂,属性多,@LocalBuilder 需要用到整个对象或者深层嵌套的属性。
两个方案怎么选
| | |
|---|
| 用 UIUtils.makeObserved 把数据转成可观测,拆属性传给 @LocalBuilder | 用 @ObservedV2/@Trace 装饰器驱动刷新,逐属性赋值 |
| | |
| | |
| 结构简单,@LocalBuilder 只用几个属性 | |
| | |
一句话总结
@LocalBuilder 里的 UI 不刷新,十有八九是传参方式的问题。你看一下传给 @LocalBuilder 的那个对象,它的引用是不是在数据更新时断掉了。如果断了——拆开传属性,或者换个层级传对象,问题就解决了。
这不是什么深奥的原理,就是个引用指向的问题。踩过一次,以后一眼就能看出来。
本文由「华鸿技术栈」原创发布,专注鸿蒙原生应用与元服务开发。分享实战代码、源码解析、性能优化及生态前沿。不讲虚的,只上干货。转载请联系授权。