ArkTS 声明式 UI 的本质:你不是在写界面,而是在写状态映射
如果你是从 TypeScript 的基础语法直接跳到 ArkTS 的 UI 开发,第一眼看到 build() 里的代码时,你可能会感到困惑。为什么这里全是函数调用 Column()、Text(),但看起来又不像在调用普通函数?为什么修改了一个变量,界面就自动变了?你找不到任何"刷新屏幕"或"更新显示"的指令,但预览器里的内容确实在动。理解这种"自动"背后的机制,是掌握 ArkTS 的核心。
一、build() 函数:你写的是描述,不是命令
在传统的编程思维中,我们习惯"命令式"的写法:先创建对象,再设置属性,再执行动作。比如你在 TypeScript 中定义一个类,实例化后调用方法:
typescript
const box = new Box();
box.setWidth(100);
box.setText('Hello');
box.show();
但在 ArkTS 中,你写的是:
typescript
build() {
Column() {
Text('Hello')
.width(100)
}
}
这段代码的关键在于:Text('Hello') 不是在屏幕上画出一个文本框,而是在内存中生成一个"描述对象"。这个对象只包含三条信息:类型是 Text,内容是 'Hello',宽度是 100。它还没有被渲染,它只是一份"设计图纸"。
build() 的完整作用,就是把这些描述对象组装成一棵树(UI Description Tree),然后交给 ArkTS 框架。框架拿到这棵树后,会自己决定怎么把它画到屏幕上。如果某个状态变了,框架会重新调用你的 build(),拿到一棵新的树,对比两棵树的差异,只更新变化的部分。
这意味着 build() 可能会被反复执行。所以里面不能写副作用------不能修改外部变量,不能执行异步请求,不能做计算之外的任何事情。因为框架不保证它只执行一次。
二、@State:变量变了,框架怎么知道的?
ArkTS 中 @State 装饰器的作用,是把一个普通变量变成框架可追踪的依赖源。
typescript
@Entry
@Component
struct Counter {
@State count: number = 0
build() {
Column() {
Text(`${this.count}`)
.fontSize(30)
Button('增加')
.onClick(() => {
this.count++
})
}
}
}
这里的 @State count 不是普通的 let count。当你第一次执行 build() 时,框架会悄悄记录:Text 这个组件读取了 count 的值 。当你点击按钮执行 this.count++ 时,框架检测到这个依赖源发生了变化,于是重新执行 build(),生成新的描述树,对比后发现 Text 的内容从 "0" 变成了 "1",只更新这一个地方。
整个过程你不需要手动通知框架"我改了数据请刷新",也不需要去找到具体的 Text 节点修改它的值。你只需要修改数据,框架通过依赖追踪自动完成更新。
核心原则:状态是唯一的真相,UI 只是状态的投影。 想改变界面,不要去想"怎么操作那个文本框",要去想"怎么改变这个数据"。
三、复用结构:@Builder 是模板,不是函数
当界面中有重复的结构时,你可能会本能地想写一个普通函数来复用。但 @Builder 不是普通函数:
typescript
@Builder
RowItem(title: string, desc: string) {
Row() {
Text(title).width(100)
Text(desc).width(200)
}
.height(50)
}
build() {
Column() {
this.RowItem('标题1', '描述1')
this.RowItem('标题2', '描述2')
}
}
@Builder 不能返回数据,不能被赋值给变量,不能作为参数传递。它只能出现在 build() 内部或另一个 @Builder 内部。它的作用是定义一段可复用的 UI 描述模板,调用时框架会把这段描述展开,插入到当前 UI 树中。
如果你需要重复项来自数组,用 ForEach:
typescript
@State items: { id: string; name: string }[] = [
{ id: '1', name: 'A' },
{ id: '2', name: 'B' }
]
build() {
List() {
ForEach(this.items, (item, index) => {
ListItem() {
Text(item.name)
}
}, (item, index) => item.id)
}
}
ForEach 不是循环语句。它描述的是:数组中的每个元素,对应 UI 树中的这样一个片段 。第三个参数 key 是数组元素的唯一标识,帮助框架在数组增删时高效更新,而不是销毁整个列表重建。
四、条件渲染:if/else 决定分支是否存在
typescript
@State isLoggedIn: boolean = false
build() {
Column() {
if (this.isLoggedIn) {
Text('欢迎回来')
Button('退出')
} else {
Button('登录')
}
}
}
这里的 if/else 不是控制程序流程,而是描述 UI 树的分支结构 。当 isLoggedIn 从 false 变为 true 时,框架对比前后两次 build() 返回的树:上一次有 Button('登录'),这一次有 Text('欢迎回来') 和 Button('退出')。框架会销毁旧的按钮,创建新的文本和按钮。
这意味着条件渲染是节点的生与死 ,不是隐藏与显示。if 为假时,对应的 UI 描述片段根本不存在于树中。
五、总结:声明式 UI 的三层思维
| 层级 | 你在做什么 | 框架在做什么 |
|---|---|---|
| 定义状态 | 用 @State 声明数据 |
追踪谁依赖了这些数据 |
| 描述映射 | 在 build() 中写状态到 UI 的对应关系 |
对比前后两棵描述树,找出差异 |
| 触发更新 | 修改 @State 的值 |
重新执行 build(),最小化更新变化的部分 |
当你接受"UI 只是状态的投影"这一事实时,ArkTS 中那些看起来"不像在写界面"的语法就变得合理了。你不是在指挥框架"画这个、改那个",而是在告诉框架:"当数据是这样时,界面应该长这样。" 框架负责把"应该"变成"现实"。