ArkUI V2 状态观测驱动(第1讲):从全量重绘到精准刷新
很多开发者从 V1 切到 V2,往往觉得只要把 @State 换成 @Local,把 @Link 换成 @Param 就万事大吉了。结果真机一跑,稍微改个嵌套对象的字段,整个页面跟着闪烁,或者疯狂掉帧。其实,V2 的观测模型发生了根本性的转变,如果你还按照 V1 那种“组件级”的粗放思维去写,不但享受不到精准刷新的红利,反而会踩进性能深坑。今天我们就从底层观测逻辑出发,把这套状态驱动理顺。

@Trace不是银弹,别把整个模型全副武装
V1 的 @Observed 只能观测第一层属性,嵌套深了就得层层 @Observed,写起来很痛苦。V2 引入了 @ObservedV2 和 @Trace,观测能力直接拉满,不仅能观测嵌套类,还能精准到具体的字段。但很多人的通病是:新建一个类,为了图省事,把类里面的所有属性全打上 @Trace。
你要知道,@Trace 的作用是建立依赖收集。一旦某个字段被 UI 绑定了,这个字段的变动就会触发对应节点的标脏。如果你把根本不会改变、或者不直接参与 UI 渲染的字段(比如单纯的内部状态、临时缓存)也加上 @Trace,等于平白无故增加了运行时的代理开销。
typescript
@ObservedV2
export class UserProfile {
// 只有直接参与 UI 渲染且会变化的字段才加 @Trace
@Trace username: string = '';
@Trace avatar: string = '';
// 像这种内部的主键或者元数据,UI 不依赖,千万别加 @Trace
userId: string = '';
rawData: Record<string, string> = {};
}
@Entry
@ComponentV2
struct ProfilePage {
@Local profile: UserProfile = new UserProfile();
build() {
Column() {
// 只有 username 变了,这里的局部才重绘
Text(this.profile.username).fontSize(20);
Image(this.profile.avatar).width(80);
Button('换个名字')
.onClick(() => {
this.profile.username = '新昵称'; // 精准触发
})
}
}
}
看这段代码,userId 和 rawData 没有加 @Trace。执行结果是,当你在其他地方修改了这两个字段,UI 完全不会理会,省下了大量的无谓刷新。常见错误就是在 UI 层直接绑定了某个深层级的属性,但在类定义时忘了加 @Trace,导致数据变了页面没反应,排错排半天。
用@Computed把计算成本压到最低
业务代码里,我们经常要在页面上展示一些“派生数据”,比如根据列表过滤出已完成的任务,或者计算总价。在 V1 时代,你可能得在 build 里直接写 filter 或者 reduce,每次页面哪怕因为一个无关的 isLoading 变化而重绘,这段复杂的计算逻辑都要跟着跑一遍。
V2 提供了 @Computed 装饰器,它的本质是一个带有缓存的 getter。只有当它依赖的源状态发生改变时,它才会重新计算;如果源状态没变,哪怕页面其他部分重绘,它也能直接吐出缓存值。
typescript
@ObservedV2
class TodoStore {
@Trace todos: string[] = [];
@Trace filterType: 'all' | 'done' = 'all';
// @Computed 依赖 todos 和 filterType
@Computed
get filteredTodos(): string[] {
if (this.filterType === 'all') {
return this.todos;
}
// 假设这里有一些复杂的过滤逻辑
return this.todos.filter(t => t.includes('done'));
}
}
执行结果非常舒心,当你往 todos 里推入新数据,filteredTodos 会自动更新,UI 随即刷新。
但这里有个极其致命的死穴:绝对不要在 @Computed 内部修改任何状态变量。比如你写着写着顺手在里面 this.todos = this.todos.filter(...),这就触发了循环依赖,应用会直接崩溃给你看。它只能是纯函数,只负责拿数据计算,不负责改数据。
@Monitor防重入的注意点
状态之间的联动,在 V2 里交给了 @Monitor。比如宽度变了,我要同步算一个高度。这比 V1 的 @Watch 强太多了,因为它可以监听深层路径。但多人协作时,最容易出的事故就是“互相监听”。
A 变了通知改 B,B 变了又通知改 A,直接进入无限递归。要避免这个问题,一定要在回调里做新旧值比较,或者引入一个 isUpdating 的防抖标志位。
typescript
@ObservedV2
class LayoutModel {
@Trace width: number = 100;
@Trace height: number = 50;
@Monitor('width')
onWidthChange(monitor: IMonitor) {
const nextHeight = this.width / 2;
// 关键:先比较,不同才赋值
if (this.height !== nextHeight) {
this.height = nextHeight;
}
}
}
这段代码保证了只有在计算出的 nextHeight 和当前的 height 确实不一样时,才会去触发标脏。执行结果就是切断了可能存在的循环引用。同时,@Monitor 应该用来处理副作用(比如发网络请求、写本地存储),不要用它来做状态派发,状态派发交回给 @Computed 更安全。
总结一下,V2 的体系里,@Trace 要克制地打在关键路径上,@Computed 负责纯计算的缓存,@Monitor 带着护甲处理副作用。先理清数据流向,再动手写代码,才是发挥 V2 威力的正确姿势。下节课,我们将带着这套状态模型,去剖析长列表的极致渲染。
更多推荐
所有评论(0)