本节摘要:深入 @State 的刷新机制:赋值如何被框架感知、刷新范围如何划定、嵌套对象的观察边界在哪里,并通过对照实验建立"精确控制刷新范围"的编码直觉。
先摆一个每本教程都有的计数器,但我们要问的问题不一样:
@Entry @Component struct CounterPage { @State count: number = 0 @State user: User = new User('小舟', 0) build() { Column({ space: 20 }) { Text(`点击次数:${this.count}`) .fontSize(24) Button('加一') .onClick(() => { this.count += 1 this.user.score += 1 }) Text(`用户得分:${this.user.score}`) .fontSize(24) } .padding(40) } } class User { name: string score: number constructor(name: string, score: number) { this.name = name this.score = score } }
点三次按钮,两行文字都变成 3,看似无差别。但把按钮回调改成下面这行,差别就露出来了:
this.user.score += 1 // 只改对象属性,不重新赋值 this.user
运行后:count 文字不再变化(没人改它),user 得分却仍然每次加一——这说明 @State 对"第一层属性"的修改也敏感。HarmonyOS 的 @State 观察能力可以概括为一层半:变量本身的重新赋值一定触发刷新;当类型是对象时,其第一层属性的变化也会被观察到,但再深一层的嵌套对象内部变化就看不到了。给 User 加一个 address 对象属性,改 address.city 不会刷新界面——这是 V1 体系最常被踩的边界(V2 用 @ObservedV2/@Trace 解决,4.3 节对比)。

"最小化更新"不等于"自动最优"。做一个对照实验。版本 A,把一个重渲染的卡片直接写在页面 build 里;版本 B,把同样的卡片抽成子组件,只把变化的数值作为参数传入:
// 版本B:静态部分隔离在子组件 @Component struct HeavyCard { @Prop label: string = '' build() { Column() { Text(this.label).fontSize(40) // ...这里假设还有几十个纯展示节点 }.padding(20) } } @Entry @Component struct PageB { @State count: number = 0 build() { Column({ space: 16 }) { HeavyCard({ label: `第${this.count}次` }) Button('加一').onClick(() => { this.count += 1 }) } } }
在重卡片里塞上足够多的展示节点后用 Profiler 观察:版本 A 每次点击整个 build 都重新执行,所有节点参与 diff;版本 B 只有 Text 的 label 相关片段更新。两者的功能一致,刷新成本差一个量级。结论不是"永远要拆组件",而是:刷新范围跟随状态的使用位置,状态放得越深,刷新越局部。高频变化的状态(动画计数、进度)应当下沉到最小组件里,低频状态(页面标题)留在上层无妨。
⚠️ 常见坑:在按钮回调里对 @State 数组用 push 修改。V1 下数组本身的 API 调用(如 push、splice)可以被观察到,但如果你把数组元素换成对象再改对象内部深层属性,就回到了观察边界问题。纪律是:改不了结构就换新数组(展开运算符生成新数组赋值回去),这是最稳的写法。
💡 关键直觉:把 @State 变量想象成"每个都接了一根信号线,线的另一端是所有读过它的界面片段"。你赋值就是拉线,拉线只惊动线另一端的东西。写得越多的状态越要管好它的读者圈。
综合本节实验,给出三条可执行的纪律:其一,状态最小拥有——数据只在真正使用它的最深层组件声明 @State,上层透传交给下一节的传递装饰器;其二,深层变更换新值——嵌套对象内部变化一律通过重新赋值整个对象或数组触发刷新,代码意图也更清晰;其三,高频状态隔离——动画与高频计数相关的状态单独成组件,别混进大页面。
用一个收尾练习固化:把 3.2 节的文章列表加一个"阅读量"字段,列表项组件内 @State 计数,点击加一;对比"计数器写在页面级"与"写在列表项组件内"两种实现的刷新范围(用 Profiler 或肉眼观察点击时其他项是否闪烁)。做完这个练习,你对刷新范围的手感就建立了。
本节要点回顾:
单组件内的状态已经吃透,但真实界面的状态要在父子与跨层之间流动。下一节把镜头拉远,看状态在组件树里的四种通行证。
补充一个日常可用的走查动作,我称之为"改动演练":拿起你的页面,假想产品经理提了三个改动——加一个字段、挪一个模块、提高某个数字的更新频率——逐一回答"我要动几个文件、刷新会波及哪些区域"。回答越快越准,状态的安放就越合理;如果"提高更新频率"这个改动让你想把整个 build 拆掉重写,说明高频状态埋在了太浅的地方,回炉 4.1 的"状态下沉"纪律。
这个走查也解释了为什么状态管理值得单开一章:它不是语法问题而是结构问题。语法错了编译器会告诉你,结构错了只会以"改需求很痛"的形式在几个月后浮现。趁应用还小,把改动演练当成每次提交前的十秒习惯,结构债就积不起来。