
一、我们想解决什么问题
打个比方,先从生活里找感觉
想象一下,你家里有一个电灯。你想通过手机 App 来控制它的开关。最直接的做法是什么?
你按一下 App 上的按钮,电灯亮了------这中间发生了什么?其实是:你的"操作"改变了一个"状态"(灯是开还是关),然后"显示"根据这个状态自动刷新了。
这个逻辑在 HarmonyOS 里,就是 @State 要做的事。
再举一个更贴近的例子:你想做一个购物车页面,购物车里有多少件商品,这个数字会随着用户加减商品而变化。如果这个数字变了,界面右上角的购物车图标旁边那个小红点就要跟着变。这个"数字一变,小红点就跟着变"的需求,就是 @State 要解决的问题。
传统写法的问题
在最早的开发模式里,假设我们写一个计数器页面,数字从 0 开始,每按一次按钮加 1。传统思路可能是这样:
- 声明一个变量
count = 0 - 写一个按钮,点击时
count++ - 在界面上显示
count
听起来很简单对吧?但问题来了------变量改了,界面并不会自动更新。因为系统不知道这个变量和界面有什么关系,它只会傻傻地渲染一次就完事了。
这就好像你改了电视后面的电路,但遥控器完全不知道,你还是得手动走过去调。
在 Android 原生开发的老办法里,你得手动调用 invalidate() 或者写一堆观察者模式来通知界面更新。代码一长,到处都是"刷新界面"的通知调用,维护起来头疼得很。
@State 的解决思路
HarmonyOS 的 ArkUI 框架引入了一套"声明式 UI"的理念,核心思路就是:我告诉系统,哪些变量会变、它们一变界面就要跟着变 。具体怎么告诉系统?用 @State 装饰器标记这些变量。
先解释一下"装饰器"这个词。很多新手第一次看到 @State 前面那个 @ 符号会觉得奇怪,这是 TypeScript/ArkTS 里的语法,叫装饰器(Decorator)。你可以把它理解为一个"标记"------用 @State 标记一个变量,就是告诉编译器:这个变量不是普通的变量,它是响应式的,需要特殊对待。编译器看到 @State 就会自动为它生成一套响应式逻辑,你完全不需要自己写这部分代码。
这套自动生成的机制是 ArkUI 框架在编译阶段悄悄帮你做的,就像编译器自动给你的代码加了一行又一行的后台逻辑,你看不到,但它确实存在并且正常工作。这种"约定大于配置"的思路是现代框架的主流做法------你只要遵守约定,就能享受框架带来的便利。
先解释一下"装饰器"这个词。很多新手第一次看到 @State 前面那个 @ 符号会觉得奇怪,这是 TypeScript/ArkTS 里的语法,叫装饰器(Decorator)。你可以把它理解为一个"标记"------用 @State 标记一个变量,就是告诉编译器:这个变量不是普通的变量,它是响应式的,需要特殊对待。编译器看到 @State 就会自动为它生成一套响应式逻辑,你完全不需要自己写这部分代码。
这套自动生成的机制是 ArkUI 框架在编译阶段悄悄帮你做的,就像编译器自动给你的代码加了一行又一行的后台逻辑,你看不到,但它确实存在并且正常工作。这种"约定大于配置"的思路是现代框架的主流做法------你只要遵守约定,就能享受框架带来的便利。
先解释一下"装饰器"这个词。很多新手第一次看到 @State 前面那个 @ 符号会觉得奇怪,这是 TypeScript/ArkTS 里的语法,叫装饰器(Decorator)。你可以把它理解为一个"标记"------用 @State 标记一个变量,就是告诉编译器:这个变量不是普通的变量,它是响应式的,需要特殊对待。编译器看到 @State 就会自动为它生成一套响应式逻辑,你完全不需要自己写这部分代码。
这套自动生成的机制是 ArkUI 框架在编译阶段悄悄帮你做的,就像编译器自动给你的代码加了一行又一行的后台逻辑,你看不到,但它确实存在并且正常工作。这种"约定大于配置"的思路是现代框架的主流做法------你只要遵守约定,就能享受框架带来的便利。
打个比方,@State 就像给变量装了一个"小广播"。一旦变量被改动了,它就会大声喊:"变了变了!界面快刷新!"然后系统收到信号,就会自动更新 UI。
整个过程不需要你手动调用什么刷新方法,一切都是自动的。这就是"响应式"------变量响应变化,界面响应变量。
再用一个生活里的例子来加深印象:想象你在用一个股票行情 App,股价从 100 变成了 102,界面上那个数字就自动变色了,不需要你下拉刷新,不需要你退出重进。这种体验背后的逻辑,就是响应式编程在支撑。
声明式 UI 的思维方式
这里有一个思维方式的转变需要特别说明一下。
在传统的"命令式"编程里,你写代码告诉电脑"第一步做什么、第二步做什么、第三步做什么"。比如:先找到文本框,把里面的文字改成 "hello",再把背景色改成红色。这种写法非常具体,但一旦界面复杂了,你要写的步骤就爆炸了。
而在"声明式"编程里,你只描述"结果长什么样"。比如:你告诉系统:"这个文本框的文字,等于变量 message,文字颜色由 isError 决定------如果 isError 是 true 就红色,否则黑色。"至于怎么去改、什么时候改、怎么高效地改,这些全交给框架处理。
@State 就是这个"声明"的关键工具。你告诉框架:"count 这个变量是响应式的,它一变,我的界面就要重新渲染。"框架收到指令,就会忠实地执行。
这个思维方式的转变,是理解 HarmonyOS 开发最重要的一步。很多人在学 @State 的时候觉得别扭,其实就是因为还在用命令式的思维去理解声明式的东西。换个角度,一切就通了。
二、数据模型设计
用 interface 简洁定义数据结构
我们先设计这个计数器案例要用到的数据结构。虽然这个案例很简单,只有一个数字需要管理,但养成好习惯很重要------用 TypeScript 的 interface 把数据结构定义清楚,以后扩展也方便。
typescript
// CounterData.ets
// 定义计数器的数据结构
export interface CounterData {
count: number; // 当前计数值
step: number; // 每次点击增减的步长
}
就这么简单。这个 interface 相当于一张"卡片",明确告诉我们:一个计数器包含两个东西------当前的数字,以及每次增减的步长。
在实际项目里,你可能有很多复杂的数据结构,比如一个商品列表、用户信息表、购物车内容等等,都可以先用 interface 定义清楚,这样代码读起来一目了然。好处是:类型检查会帮你把关,字段写错了编辑器直接报错,而不是等到运行时才发现问题。
一个常见的数据结构扩展思路
假设我们要做一个购物车,购物车里有哪些东西呢?可以这样设计:
typescript
export interface CartItem {
id: string; // 商品ID
name: string; // 商品名称
price: number; // 单价
quantity: number; // 购买数量
}
export interface CartData {
items: CartItem[]; // 购物车里的商品列表
totalPrice: number; // 总价
}
然后在组件里:
typescript
@State cart: CartData = {
items: [],
totalPrice: 0
};
像这样先把数据模型定义清楚,后面的业务逻辑写起来就顺了。虽然我们的计数器案例用不到这么复杂,但这个思路要建立起来,因为真实项目中数据结构往往是嵌套的。
养成先定义数据结构再写业务逻辑的习惯,好处非常明显:
第一,接口文档即代码 。你在 interface 里写的每个字段的名字、类型、注释,就是最实时、最准确的文档。不需要额外维护一份文档,代码一变文档自动过期这种事不会发生。
第二,编译期检查。TypeScript 的类型系统会在编译阶段就发现你拼错了字段名、用错了类型、传错了参数,而不是等到用户操作到那一步才发现 bug。越早发现 bug,修复成本越低。
第三,重构有底气 。当你需要修改数据结构的时候,interface 定义了所有字段的形状,改完 TS 编译器会告诉你哪些地方用了旧的字段名,一键修复,省心省力。
所以强烈建议:不管项目大小,写 @State 之前先写 interface,这个习惯会让你终身受益。
三、核心设计决策
方案对比:不用 @State vs 用 @State
我们先看一下为什么非要用 @State,对比两种写法的区别:
| 维度 | 不用 @State(普通变量) | 用 @State 装饰器 |
|---|---|---|
| 界面是否自动刷新 | ❌ 不刷新,变量改界面不变 | ✅ 自动刷新,变量改界面跟着变 |
| 需要手动刷新吗 | ❌ 需要写额外代码通知系统 | ✅ 不需要,装饰器自动搞定 |
| 可读性 | 一般,变量用途不明确 | 高,一看就知道这是响应式状态 |
| 维护性 | 差,多了容易乱 | 好,状态集中管理 |
| 适用场景 | 组件内部不变的值 | 需要驱动 UI 变化的值 |
| 学习曲线 | 无(但用起来头疼) | 有一点,但用起来爽 |
选型理由
选 @State 的原因很朴素:省事、可读、易维护。
"省事"是因为你不需要写任何手动刷新逻辑。传统开发里你要写一堆 observer、listener、callback,声明式 UI 里一个 @State 就全部搞定了。代码量直接少一个数量级,维护起来自然也轻松。
"可读"是因为 @State 本身就是文档。别人看你的代码,一眼就知道哪些变量是用来驱动界面的、哪些是纯计算的中间变量,不需要猜。这是团队协作里特别有价值的事情------代码是给机器执行的,但也是给人看的,@State 让代码的自解释性大大增强。
"易维护"是因为逻辑集中。你改一个状态变量的值,不需要到处找"这个值改了之后应该刷新哪几个地方",框架自动帮你处理所有的关联刷新。如果将来需求变了,需要在状态变化时多做一件事(比如埋点上报),你只需要在状态变更的地方处理就行,不需要改一堆 UI 刷新的调用点。
声明式 UI 的精髓就是"你描述结果,系统帮你实现过程"。你不需要关心"怎么刷新",只需要告诉系统"这个变量变了要刷新",剩下的框架会帮你做。
而且用 @State 标记过的变量,在代码里一眼就能看出来"这个是用来驱动界面的状态",团队协作的时候沟通成本也低。代码 review 的时候别人一看 @State,就知道这部分是状态管理相关的逻辑,不需要到处去找"哪里改了值又改了 UI"。
什么时候不该用 @State
光知道什么时候用还不够,知道什么时候不用同样重要。
不需要用 @State 的情况:
第一个,纯计算的中间值。比如你有个变量 doubleCount = this.count * 2,这只是 count 的两倍,它不需要自己的 @State。因为只要 count 变了,doubleCount 自然跟着变,它自己不需要是响应式的。
第二个,固定不变的配置常量。比如 MAX_COUNT = 100、API_BASE_URL = "https://api.example.com" 这种东西,整个生命周期里都不会变,加了 @State 只是浪费。
第三个,从父组件传进来只需要读取的值。这种情况应该用 @Prop,而不是 @State。@State 是给自己用的状态,@Prop 是接收外部传入的值。
四、完整代码实现
全部代码就这一个文件
我们的计数器案例,写在一个 .ets 文件里就够了。代码不长,我拆开来讲:
typescript
// Counter.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct CounterPage {
@State count: number = 0; // ① 标记为状态变量,值从0开始
@State step: number = 1; // ② 步长也标记为状态
build() {
Column() {
// 显示当前计数值
Text(`当前计数:${this.count}`)
.fontSize(40)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 30 })
// 操作按钮区域
Row({ space: 20 }) {
Button('减少')
.onClick(() => {
this.count -= this.step; // ③ 点击后值变了
})
Button('增加')
.onClick(() => {
this.count += this.step; // ④ 点击后值又变了
})
}
// 重置按钮
Button('重置')
.margin({ top: 20 })
.onClick(() => {
this.count = 0; // ⑤ 重置为0
promptAction.showToast({ // ⑥ 顺便弹个提示
message: '已重置'
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
关键点逐行解读
逐个解释一下上面带数字标注的地方:
① @State count: number = 0
这是整个机制的核心。用 @State 装饰器告诉系统:count 是一个响应式状态变量。它的初始值是 0。一旦 count 的值发生变化,UI 就会自动刷新。
注意这个变量的位置------它是在 @Component 装饰的 struct 内部、但在 build() 方法外部声明的。这是 @State 变量必须待的地方。写在 build() 方法里面的局部变量,哪怕加了 @State 也没用,因为局部变量超出函数作用域就销毁了。
② @State step: number = 1
步长也用 @State 标记。这意味着如果我们加一个功能让用户可以调节步长(比如从 1 变成 5),界面也能及时响应。不过在这个简单例子里,step 暂时不变。
③ this.count -= this.step
点击"减少"按钮时,修改 this.count。注意这里有个细节:this 是必须的,因为 count 是一个被 @State 装饰器管理的类属性,不是普通局部变量。通过 this 才能访问到类实例上的这个属性。
如果你忘了写 this,直接写 count -= step,ArkTS 会报编译错误,因为 count 在这个作用域里找不到定义。
④ this.count += this.step
同理,点击"增加"按钮时增加计数。由于 count 是 @State 变量,这行代码执行后,框架检测到值变化,会自动触发 UI 重新渲染,屏幕上的数字就变成新的了。
整个过程你不需要调用任何"刷新"方法,不需要写 this.update() 或者 this.forceUpdate() 之类的,框架全自动处理。
⑤ this.count = 0
重置逻辑。把计数归零,UI 会同步刷新。
⑥ promptAction.showToast
顺手加一个轻提示,让用户知道重置成功了。这是 HarmonyOS 里的一个系统 API,非常方便,不需要自己写弹窗组件。
如果要加一个步长调节功能
既然 step 也标记了 @State,我们可以在 UI 里加一个 Slider 组件来动态调节步长:
typescript
Slider({
value: this.step,
min: 1,
max: 10,
step: 1
})
.onChange((value: number) => {
this.step = value;
})
Text(`步长:${this.step}`)
加了这几行之后,用户拖动滑块改变步长,滑块旁边的步长数字会实时更新,而整个计数器的工作方式完全不受影响。这就是 @State 的灵活性------多个状态变量各自独立,互不干扰。
五、深度技术原理
底层是怎么做到的?
很多人到这里会好奇:我就是写了个 this.count++,它怎么就知道要去刷新界面?这背后到底发生了什么?这一节我们来把这个过程掰开揉碎了讲。
第一层:装饰器模式
@State 本质上是一个装饰器(Decorator)。在 TypeScript/ArkTS 里,装饰器的作用是"在代码运行之前,偷偷给你的变量或函数包一层逻辑"。
当你写 @State count: number = 0 时,框架会:
- 创建一个"代理"对象,把
count的读写操作拦截下来 - 每当你修改
count的值,这个"代理"就会收到通知 - 收到通知后,代理就会告诉 UI 框架:"状态变了,需要重新渲染"
这就像一个"中间人"------你跟它打交道,它再帮你去通知真正要做事的人。
打个比方:你去银行存钱,把钱交给柜员,柜员帮你办理存款业务。柜员就是那个"代理",你不需要自己跑去金库把现金放进去。@State 就是这个柜员的角色,帮你处理"通知界面刷新"这件幕后工作。
第二层:响应式系统
ArkUI 底层有一套响应式系统,类似于 Vue 的响应式原理或者 React 的状态管理思路(但实现方式不同)。
简单来说,框架在编译阶段会分析你的 @State 变量,然后自动生成一套"监听-通知"机制。你写的 this.count++ 实际上是触发了一个 setter,setter 里会调用一个内部的 aboutToBeChanged() 方法,这个方法再触发 UI 重新渲染。
具体来说,整个流程是:
- 赋值时 :执行
this.count = 5,触发 setter - Setter 中:调用框架内部的状态变更通知方法
- 通知层:框架收到通知,开始执行"重新渲染"任务
- 渲染层 :执行
build()方法里的代码,生成新的 UI 描述 - 比对层:将新的 UI 描述和旧的对比,找出差异
- 更新层:只更新有差异的部分(这就是增量渲染)
第三层:增量渲染
最神奇的地方在于:UI 刷新的时候,并不是把整个页面全部重新画一遍。框架会通过差异化比对(类似 DOM Diff),只更新变化了的部分。
这就好比你去理发店染发,发型师不会让你先把头发全剃光再重新长出来,而是精准地只处理需要改变的部分。框架的增量渲染也是这个思路------它很"聪明",知道只更新真正变化了的地方。
这个设计对性能的影响是巨大的。想象一下一个页面有一百个组件,其中只有一个 Text 显示的文本变了。如果每次刷新都重建全部一百个组件,那性能开销就是一百倍。增量渲染把这个开销降到了百分之一。对于复杂的列表页面,这种优化带来的性能提升是肉眼可见的。
打个比方:计数器从 5 变成 6,框架只需要把屏幕上显示 "5" 的那个 Text 组件改成显示 "6",而不需要重新构建整个 Column、整个 Button。这就好比你在一张纸上画了一幅画,后来想改其中一根线条,你不需要把整张纸扔掉重画,你只需要擦掉那根线再画一根新的。框架就是这么高效的。
简单总结原理链
整个链路就是一条链:修改变量 → 装饰器拦截 → 通知框架 → 差异化渲染 UI。
你只需要记住第一步和最后一步,中间那些脏活累活,框架都帮你做了。这就是声明式 UI 的魅力------你描述"要什么",不用操心"怎么做"。
用一句更简单的话总结 @State:变量前面加个 @State,变了界面自动更新,就这么简单。 所有复杂原理,最终都是为了实现这个简单效果。
最后再强调一点:虽然原理说起来有"拦截 → 通知 → 渲染"这一串流程,但在实际写代码的时候,你感受到的只有"改值 → 界面变"这简单两步。这正是好框架的标准------把复杂性藏起来,把简洁性留给开发者。ArkUI 的 @State 正是做到了这一点。
和其他平台做个对比
如果你是从其他平台转过来的开发者,对比一下会更容易理解:
| 平台 | 状态管理方案 | 特点 |
|---|---|---|
| Flutter | StatefulWidget + setState() |
需要切换到另一个类来处理状态,代码分散 |
| React Web | useState hook |
函数式组件,hooks 模式 |
| React Native | 同上,借助 Context 或 Redux | 大型应用需要额外状态库 |
| Android 原生 | LiveData / StateFlow (Jetpack Compose) |
Compose 也是声明式,类似 ArkUI |
| HarmonyOS ArkUI | @State 装饰器 |
原生支持,语法简洁,框架完成度高 |
从对比里可以看出,ArkUI 的 @State 方案在简洁性上是很有优势的。它不需要你切换到另一个类,不需要写 observer,不需要写任何订阅代码,只需要在变量前面加一个 @State 就完事了。对于习惯了命令式编程的开发者来说,这个学习曲线是最低的。
- Flutter(React-like) :用的是
setState(),调用后触发重建,结合const构造和constwidget 可以做优化 - React(Web) :用的是
useStatehook,原理类似,但 React 用的是虚拟 DOM diffing - Android(传统 View 系统) :用
findViewById找控件,然后手动setText()更新,没有自动性 - HarmonyOS(ArkUI) :用
@State,声明即响应,框架自动处理 diffing 和更新
可以看出,HarmonyOS 的方案是这些方案里最简洁的------你只需要加一个 @State,剩下的全交给框架。
六、常见问题解答(FAQ)
Q1:@State 只能用于简单类型吗?
不是 。@State 支持 number、string、boolean 等基本类型,也支持 class(但建议配合 @Observed 一起用)、Array、Object 等复杂类型。
不过复杂类型用起来有讲究。如果你写:
typescript
@State items: string[] = [];
this.items.push("new item"); // ❌ 这种写法不会触发刷新!
这是因为 push 是在原数组上操作,数组引用本身没变,框架检测不到变化。正确写法是:
typescript
this.items = [...this.items, "new item"]; // ✅ 生成了新数组
或者:
typescript
this.items = this.items.concat("new item"); // ✅ concat 也生成新数组
核心原则就是:要创建一个新的引用,框架才能检测到变化。
Q2:@State 变量在子组件里能用吗?
能,但要小心传递方式 。@State 变量可以传递给子组件,但需要通过参数传递。不过要注意单向数据流原则------子组件收到的是值的副本,子组件修改不会影响父组件的状态。
如果需要双向绑定,要用 @Link 装饰器。这个我们下一篇文章会专门讲。
Q3:修改变量后界面没反应,是怎么回事?
常见原因有三个:
第一个:忘了加 @State
变量声明时忘记装饰器,系统就不认识它,变量改了界面当然不动。这是最常见的低级错误,解决方法就是检查变量前面的 @State 有没有漏掉。
第二个:在函数内部声明了局部变量
局部变量不是响应式的,@State 只能用在 @Component 结构体外面。像这样写是错的:
typescript
build() {
let count = 0; // ❌ 局部变量,加了 @State 也没用
// ...
}
正确写法是把变量放到 build() 方法外面:
typescript
@State count: number = 0; // ✅ 在结构体级别
build() {
// ...
}
第三个:赋值方式不对
最典型的就是数组操作,如上面 Q1 里讲的,用了 push/splice 这种原地修改的方法,而不是生成新数组。还有一种常见错误是对对象的某个属性赋值而不是替换整个对象------对于嵌套对象,this.data.nested.value = 5 这种深层赋值需要配合 @Observed 使用,否则框架无法检测到变化。
Q4:所有变量都要加 @State 吗?
千万不用 。@State 是专门给"会变、且一变界面就要跟着变"的值用的。那些只读不变的常量、纯计算的中间变量,完全不需要加装饰器。
不加装饰器的好处是性能更好------框架不需要为这些变量维护响应式链路,内存和计算开销都更小。加多了反而增加框架开销,有点像为了防雨把整个城市都用防水布盖起来,大可不必。
记住一个简单的判断标准:只有状态才需要 @State 。状态就是"会变"的数据。如果一个值从初始化到销毁都不变,它就不是状态,不需要加 @State。
Q5:@State 和 @Link 有什么区别?
这是两个最容易搞混的装饰器。简单说:
@State是"我的变量,我来改"------这个变量属于当前组件私有,只有它自己能改@Link是"我接收了一个变量,但我也想改它,而且改了之后父组件也能看到"------相当于双向绑定
打个比方:@State 就像你手里的遥控器,只有你能按;@Link 就像你把遥控器借给了室友,室友按了,电视也变了,而且你也能看到电视变了(因为你们共用同一个遥控器)。
在代码层面,它们的区别是:
typescript
// 父组件
@State parentCount: number = 0;
// 传给孩子:普通传值(单向)
ChildComponent({ count: this.parentCount })
// 传给孩子:双向绑定(注意这个 $ 符号)
ChildComponent({ count: $parentCount })
子组件那边接收时:
typescript
// 普通传值:子组件只能看,不能改
@Prop count: number;
// 双向绑定:子组件可以改,改了父组件也知道
@Link count: number;
这里特别要注意 $parentCount 前面的 $ 符号------它表示"引用"而不是"值"。传值 this.parentCount 传的是当前时刻的一个快照;传引用 $parentCount 传的是指向原始变量的"指针",所以子组件改了之后父组件也能看到。
这个 $ 符号和 Swift 里的 inout 参数、React 里的 onChange 回调模式是一个意思,只是语法不同而已。理解了本质,各种语言的语法差异就不难掌握了。
Q6:@State 变量能初始化为异步获取的值吗?
可以,但要用对方法 。直接在 @State 声明时赋值不支持异步表达式(比如 await fetchData()),因为装饰器初始化发生在同步阶段,在异步数据返回之前组件就已经挂载了。
正确做法是借助生命周期钩子 aboutToAppear():
typescript
@State data: string = ''; // 先给一个默认值
aboutToAppear() {
// 这里可以写异步逻辑
fetchData().then((result) => {
this.data = result; // 数据返回后赋值,界面会自动刷新
})
}
这个 aboutToAppear() 是组件即将显示时调用的生命周期方法,非常适合用来做初始化数据的异步加载。
Q7:@State 能嵌套多层对象吗?
能,但要配合 @Observed 使用。关于 @Observed 的详细内容我们会在后续文章里深入展开,这里先预告一下它解决的是什么问题:
比如你有一个嵌套对象:
typescript
class Inner {
value: number = 0;
}
class Outer {
inner: Inner = new Inner();
}
@State data: Outer = new Outer();
this.data.inner.value = 5; // ❌ 嵌套层级的修改检测不到
要让它能检测到,需要用 @Observed 装饰内部类:
typescript
@Observed
class Inner {
value: number = 0;
}
这样即使修改嵌套对象的属性,界面也能正确响应。这个话题内容比较多,以后我们会专门写文章讲 @Observed。
Q8:@State 变量初始化有没有默认值要求?
有,必须显式初始化 。@State 装饰的变量必须在声明时直接赋予初始值,不能只声明不赋值。
这是因为 @State 变量在组件创建时就需要有一个确定的初始值,框架会用这个初始值来初始化响应式系统。如果你写了 @State count: number; 然后在别的地方赋值,ArkTS 编译器会报错。
这个限制其实也是好事------它强制你把"默认值是什么"这件事想清楚,而不是留到运行时才发现某个变量没有值导致 NaN 或者 undefined。好的默认值设计能大大减少运行时 bug。
七、运行效果
计数器页面运行示意

整个过程无需任何手动刷新代码,每次修改变量,Text 组件里的数字就自动更新了。这就是 @State 装饰器带来的"声明式"体验------你只管改值,界面交给框架。
调试小技巧
如果你发现修改变量后界面没反应,可以在 DevEco Studio 里打开实时检查面板,查看组件树和当前的状态值。另外,控制台日志里也会打印状态变化的信息,便于追踪问题。
还有一个实用的技巧:在 build() 方法里加一行 console.info('count = ' + this.count),每次界面渲染时你都能在日志里看到当前的 count 值是多少。如果点了一下按钮但日志没变化,说明界面根本没重新渲染,问题就定位在"状态变更没有被检测到"这个环节;如果日志变了但界面没变,说明问题出在渲染层。
从一个按钮到一个完整页面
计数器的例子虽然小,但它展示了一个完整的数据驱动 UI 的闭环:用户操作 → 状态变化 → 界面自动刷新。这个闭环是所有 HarmonyOS 应用的基本工作单元------无论你的应用多复杂,都是由无数个这样的闭环组成的。
学会了 @State,你就掌握了构建 HarmonyOS 应用最核心的能力。从一个按钮到一个完整页面,区别只在于状态变量的数量和业务逻辑的复杂程度,底层机制完全一样。所以别小看这个计数器案例,它是所有复杂应用的起点。
八、扩展方向
计数器虽然简单,但它是理解整个 HarmonyOS 状态管理体系的起点。沿着这条路,你可以继续探索以下几个方向,每个方向都是一个全新的知识领域:
1. 加个步长调节器
在现有基础上加一个滑块,让用户可以调节每次加减的步长(1、2、5、10)。这会让你更好地理解多个 @State 变量之间如何协同工作。而且你会发现,由于 step 也标记了 @State,调节步长完全不需要改动任何其他代码,框架自动处理好了。
2. 引入 @Link 做双向绑定
把增加/减少的逻辑拆到一个子组件里,用 @Link 把状态从父组件传下去。这样你就能亲身体验 @State 和 @Link 的配合,理解"单向数据流"的含义。
实话说,@Link 是 @State 最重要的搭档。很多新手学完 @State 就停住了,觉得自己会了,但一做稍微复杂点的页面就卡住了------就是因为还没学会 @State 和 @Link 配合使用。等你搞懂了 @Link,你就能写出组件化、可复用的界面了。
3. 接入 @Observed 处理复杂对象
当你的状态从简单数字变成一个对象(比如 { count: 0, history: [], maxValue: 100 }),就需要 @Observed 配合使用了。它能让嵌套对象也具备响应式能力,这是构建真实业务应用必不可少的一环。
4. 用 @Prop 做纯展示传递
如果子组件只是"看"状态,不需要改状态,用 @Prop 更合适。它是单向数据流,不会产生意外的状态污染。@Prop 的一个典型应用场景是:一个展示用户头像和昵称的卡片组件,它只需要显示数据,不需要修改数据,这时候用 @Prop 就很合适。
5. 结合网络请求做真实数据
把静态计数器换成真实的数据展示,比如从接口拉取一个列表并展示。用户在文本框输入搜索词,列表就自动过滤。这个过程中你需要理解 aboutToAppear 生命周期、异步数据的处理方式,以及如何优雅地管理"加载中"和"加载失败"的状态。
这类需求在实际工作中最常见。比如做一个天气 App:你在文本框里输入城市名,点搜索,后端返回天气数据,数据一变界面就刷新。这些都建立在 @State 能正确触发刷新的基础上。
6. 了解其他状态装饰器
HarmonyOS 的状态管理家族很大,除了 @State,还有 @Link、@Prop、@Provide、@Consume、@ObjectLink 等等。计数器案例是入口,吃透了之后再学其他的,就会发现它们其实是一套相互配合的系统,各有各的用途,共同构成了 HarmonyOS 的响应式状态管理生态。
6. 了解其他状态装饰器
HarmonyOS 的状态管理家族很大,除了 @State,还有 @Link、@Prop、@Provide、@Consume、@ObjectLink 等等。计数器案例是入口,吃透了之后再学其他的,就会发现它们其实是一套相互配合的系统,各有各的用途,共同构成了 HarmonyOS 的响应式状态管理生态。
7. 探索性能优化方向
当你的状态变量很多、界面很复杂的时候,响应式刷新的性能问题就会显现出来。届时你需要了解 ArkUI 的渲染优化机制,比如 LazyForEach(懒加载列表)、@Reusable(组件复用)等技巧。这些能让你的应用在低配置设备上依然流畅运行。
8. 学习状态管理设计模式
当你从单个页面跨越到多页面应用时,简单的 @State 就不够用了。这时候你需要了解应用级别的状态管理------如何在多个页面之间共享数据、如何管理全局状态、如何处理页面跳转时的数据持久化等等。这些话题涉及架构设计,是从小工到工匠的必经之路。
一个建议是:先把手头的 @State 用熟,不要一开始就上 Redux/Vuex 那一套。状态管理的复杂度应该和项目的复杂度相匹配。计数器就用 @State,购物车用 @State + @Link,再大一点用 @Provide/@Consume,真正到了大型应用再用全局状态管理。渐进式地学习,比一开始就追求"最佳实践"要实在得多。