【HarmonyOS应用开发入门】第四期:ArkTS语言基础(二)
方面命令式( Android Java)声明式(鸿蒙 ArkTS)思维方式“怎么做”:一步步操作UI“是什么”:描述UI应有的状态数据与UI关系数据改变 → 手动更新UI数据改变 → UI自动重新渲染代码量多,每次改状态都要操作UI少,只需改数据维护性容易出错,可能遗漏更新不容易出错,状态与UI自动同步典型代码@State + 数据绑定async/await 就是:用同步的写法,写异步的逻辑等 P
一、详解声明式UI
1、概念
声明式UI是一种用“描述结果”而不是“写步骤”的方式来构建用户界面的方法。
比如做菜🍳
命令式:
- 1、开火
- 2、倒油
- 3、等油热
- 4、放菜
- 5、翻炒5分钟
- 6、放盐
- 7、盛出来
需要一步步指挥每个动作
声明式:
- 我要一份炒好的青菜(盐适中,熟了但没焦)
只告诉厨房最终状态,具体步骤由厨房自己搞定
2、举个例子
需要实现的功能是:点击按钮数字+1,当数字≥10时按钮变红并禁用
命令式UI(Android Java)实现:
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="20dp">
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="40sp"
android:layout_marginBottom="30dp"/>
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击+1"
android:textSize="18sp"
android:padding="15dp"/>
</LinearLayout>
Activity文件
public class MainActivity extends AppCompatActivity {
private TextView tvCount;
private Button btnAdd;
private int count = 0; // 状态变量
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. 手动找到视图组件
tvCount = findViewById(R.id.tvCount);
btnAdd = findViewById(R.id.btnAdd);
// 2. 设置初始状态
tvCount.setText("0");
btnAdd.setEnabled(true);
btnAdd.setBackgroundColor(Color.BLUE);
// 3. 手动设置点击事件
btnAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 4. 更新数据
count++;
// 5. 手动更新UI(必须记住所有要修改的地方)
// 更新数字显示
tvCount.setText(String.valueOf(count));
// 根据条件修改按钮状态
if (count >= 10) {
// 手动禁用按钮
btnAdd.setEnabled(false);
// 手动改变按钮颜色
btnAdd.setBackgroundColor(Color.RED);
// 手动改变按钮文字
btnAdd.setText("已达上限");
} else {
// 手动保持按钮可用
btnAdd.setEnabled(true);
btnAdd.setBackgroundColor(Color.BLUE);
btnAdd.setText("点击+1");
}
// 6. 可能需要手动重绘
tvCount.invalidate();
btnAdd.invalidate();
}
});
}
}
声明式UI(鸿蒙 ArkTS)实现:
@Entry
@Component
struct CounterPage {
// 1. 定义状态变量(数据源)
@State count: number = 0
build() {
Column({ space: 20 }) {
// 2. 数字显示 - 直接绑定状态
Text(this.count.toString())
.fontSize(40)
.fontColor(Color.Black)
.margin({ bottom: 30 })
// 3. 按钮 - 根据状态自动更新
Button(this.getButtonText()) // 按钮文字自动随状态变化
.fontSize(18)
.padding(15)
.backgroundColor(this.getButtonColor()) // 颜色自动随状态变化
.enabled(this.count < 10) // 是否可用自动随状态变化
.onClick(() => {
// 4. 只需要更新状态,UI会自动更新
this.count++
})
}
.width('100%')
.height('100%')
.padding(20)
.justifyContent(FlexAlign.Center)
}
// 5. 计算属性:根据状态返回按钮文字
private getButtonText(): string {
return this.count >= 10 ? '已达上限' : '点击+1'
}
// 6. 计算属性:根据状态返回按钮颜色
private getButtonColor(): ResourceColor {
return this.count >= 10 ? Color.Red : Color.Blue
}
}
核心区别:
- 命令式(Android Java):像厨师,要自己控制每个步骤
// 你像厨师,要自己控制每个步骤:
btnAdd.setOnClickListener(v -> {
// 1. 改数据
count++;
// 2. 改显示
tvCount.setText(String.valueOf(count));
// 3. 改按钮可用性
if (count >= 10) {
btnAdd.setEnabled(false);
}
// 4. 改按钮颜色
if (count >= 10) {
btnAdd.setBackgroundColor(Color.RED);
} else {
btnAdd.setBackgroundColor(Color.BLUE);
}
// 5. 改按钮文字
btnAdd.setText(count >= 10 ? "已达上限" : "点击+1");
});
- 声明式(鸿蒙 ArkTs):点菜就好
// 你像点餐,只要描述想要什么:
Button(this.getButtonText()) // "我要显示这个文字"
.backgroundColor(this.getButtonColor()) // "我要这个颜色"
.enabled(this.count < 10) // "我要这个可用状态"
.onClick(() => {
// 只改数据,UI会自动更新!
this.count++
})
// UI会根据count值自动变成对应的样子
3、差异总结
| 方面 | 命令式( Android Java) | 声明式(鸿蒙 ArkTS) |
|---|---|---|
| 思维方式 | “怎么做”:一步步操作UI | “是什么”:描述UI应有的状态 |
| 数据与UI关系 | 数据改变 → 手动更新UI | 数据改变 → UI自动重新渲染 |
| 代码量 | 多,每次改状态都要操作UI | 少,只需改数据 |
| 维护性 | 容易出错,可能遗漏更新 | 不容易出错,状态与UI自动同步 |
| 典型代码 | findViewById() + setText() | @State + 数据绑定 |
二、装饰器深度解析
ArkTs的基本组成如下图:
- 装饰器:用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述:@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新
- UI描述:以声明式的方式来描述UI的结构,例如build()方法中的代码块。
- 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
- 系统组件:ArkUI框架中默认内置的基础和容器组件,可以直接调用,例如示例中的Column、Text、Divider、Button
- 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。
- 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。
除此之外,ArkTS扩展了多种语法范式来使开发更加便捷,都会在接下来的文章中详细介绍。
-
@Builder/@BuilderParam:特殊的封装UI描述的方法,细粒度的封装和复用UI描述。
-
@Extend/@Styles:扩展系统组件和封装属性样式,更灵活地组合系统组件。
-
stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。
1、装饰器基础概念
在鸿蒙(HarmonyOS)ArkTS开发中,装饰器是一种特殊语法,用于修饰类、属性或方法,以扩展其功能或赋予特定行为。
对,就是上篇博文说的:装饰器就是 “不拆房子,只搞装修” 🏠 → 🏡
2、内置装饰器分类
1)类装饰器
- @Entry - 入口组件,标记为页面入口组件,一个模块只能有一个@Entry
@Entry // 标记为应用入口
@Component
struct IndexPage {
build() {
// ...
}
}
- @Component:用于标记自定义组件,允许开发者通过声明式UI描述页面布局,用于定义可复用的UI组件
@Component
struct MyComponent {
// 组件内部状态
@State count: number = 0
build() {
// 构建UI
}
}
- @Observed:装饰class。需要放在class的定义前,使用new创建类对象。
2)状态装饰器V1
- @State:组件内部私有状态,变化时触发当前组件重新渲染,支持:number、string、boolean、class、Array等
@Component
struct Counter {
@State count: number = 0 // 变化时触发UI更新
build() {
Text(this.count.toString()) // 自动同步
.onClick(() => {
this.count++ // 只需改数据,UI自动更新!
})
}
}
- @Prop - 单向数据流,父 → 子的单向传递,子组件不能修改@Prop变量,父组件@State变化时,@Prop自动更新
// 父组件
@Component
struct Parent {
@State parentCount: number = 0
build() {
Column() {
Child({ count: this.parentCount }) // 传递数据
}
}
}
// 子组件
@Component
struct Child {
@Prop count: number // 接收父组件数据
build() {
Text(`来自父组件: ${this.count}`)
// this.count = 10 // ❌ 错误!@Prop不可修改
}
}
- @Link - 双向数据绑定:父子组件数据同步,子组件可以修改,父组件自动更新
// 父组件
@Component
struct Parent {
@State parentCount: number = 0
build() {
Column() {
Text(`父组件: ${this.parentCount}`)
Child({ count: this.parentCount })
}
}
}
// 子组件
@Component
struct Child {
@Link count: number // 双向绑定
build() {
Button(`子组件+1 count = ${this.count}`)
.onClick(() => {
this.count++ // ✅ 可以修改!会同步到父组件
})
}
}
- @Provide/@Consume - 跨层级传递:避免逐层传递
// 祖先组件
@Component
struct Ancestor {
@Provide name: string = '夏小鱼' // 提供数据
build() {
Column() {
Parent()
}
}
}
// 中间组件(不需要接收)
@Component
struct Parent {
build() {
Column() {
Child()
}
}
}
// 后代组件
@Component
struct Child {
@Consume name: string // 直接获取祖先数据
build() {
Text(this.name)
}
}
- @Watch - 状态监听:状态变化时执行回调
@Entry
@Component
struct WatchDemo {
@State @Watch('onCountChange')count: number = 0
@State total: number = 0
// 监听count变化
onCountChange() {
this.total += this.count
console.log(`count变为: ${this.count}, total: ${this.total}`)
}
build() {
Button(`点击 ${this.count},total ${this.total}`)
.onClick(() => {
this.count++
})
}
}
- @ObjectLink - 对象属性监听
@Observed
class User {
name: string = '';
age: number = 0;
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
@Component
struct UserComponent{
@ObjectLink user: User // 监听对象内部属性
build() {
Text(this.user.name)
}
}
@Component
struct UserInfo {
@State user:User = new User('夏小鱼',18);
build() {
Column() {
UserComponent({user:this.user})
Button('改名')
.onClick(() => {
this.user.name = '夏小七' // 触发UI更新
})
}
}
}
3)方法装饰器
- @Builder装饰器:自定义构建函数:用于封装可复用的UI结构,通过提取重复的布局代码提高开发效率
@Builder
buildContent(){
Text('动态内容').width('100%')
Button('按钮').width('100%')
}
- @BuilderParam - 动态UI:@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。
@Component
struct Container {
@BuilderParam content: () => void // 接收UI片段
build() {
Column({space:10}) {
this.content() // 渲染传入的UI
}.width('100%').height('120')
}
}
@Entry
@Component
struct IndexPage {
build() {
Column() {
Container({
content: () => {
this.buildContent();
}
})
}
}
@Builder
buildContent(){
Text('动态内容').width('100%')
Button('按钮').width('100%')
}
}
- @Styles装饰器:定义组件重用样式
@Entry
@Component
struct IndexPage {
@State heightValue: number = 50;
@Styles
fancy() {
.height(this.heightValue)
.backgroundColor(Color.Blue)
.onClick(() => {
this.heightValue = 100;
})
}
build() {
Column() {
Button('change height')
.fancy()
}
.height('100%')
.width('100%')
}
}
- @Extend装饰器:定义扩展组件样式,支持封装指定组件的私有属性、私有事件和自身定义的全局方法。
// superFancyText可以调用预定义的fancy
@Extend(Text)
function superFancyText(size: number) {
.fontSize(size)
.fancy()
}
@Entry
@Component
struct IndexPage {
build() {
Row({ space: 10 }) {
Text('Fancy')
.superFancyText(16)
Text('Fancy')
.superFancyText(24)
}
}
}
- stateStyles:多态样式
Button('Button1')
.focusOnTouch(true)
.stateStyles({
focused: {
.backgroundColor('#ffffeef0')
},
pressed: {
.backgroundColor('#ff707070')
},
normal: {
.backgroundColor('#ff2787d9')
}
})
.margin(20)
三、异步编程全面掌握
1、并发
并发是指系统在同一时间内处理多个任务的能力。在多核设备上,不同任务可以真正并行地在多个CPU上执行;而在单核设备上,CPU会通过任务切换机制,在任务处于等待状态(如I/O操作)时执行其他任务,从而最大化CPU利用率。
为保障应用流畅性,避免耗时任务阻塞UI主线程,ArkTS提供了两种并发处理方案:
异步并发
- 采用非阻塞执行模式:任务可暂停并在适当时机恢复
- 单线程执行:同一时刻仅运行一个任务片段
- 实现方式:基于Promise和async/await语法
- 适用场景:单次I/O密集型操作
多线程并发
- 真正并行执行:多个任务片段同步运行
- 线程分工:主线程负责UI响应,后台线程处理耗时操作
- 实现方式:通过TaskPool和Worker机制
- 适用场景:计算密集型或长时间运行任务
2、异步并发 (Promise和async/await)
Promise和async/await是标准的JS异步语法,提供异步并发能力。异步代码执行时会被挂起,稍后继续执行,确保同一时间只有一段代码在运行
典型的异步并发使用场景:
- I/O 非阻塞操作:网络请求、文件读写、定时器等。
- 任务轻量且无 CPU 阻塞:单次任务执行时间短。
- 逻辑依赖清晰:任务有明确的顺序或并行关系。
异步并发是一种编程语言的特性,允许程序在执行某些操作时不必等待其完成,可以继续执行其他异步代码。
1)Promise
简单来说,Promise 就是一个表示“将来可能完成的事情”的对象,它有三种最终状态:成功、失败、无论成功失败都要做的事。
我们可以用三个方法来安排对应的事情:
-
.then(成功时做什么)- 只传一个函数 → 处理成功的情况。
- 传两个函数
.then(成功函数, 失败函数)→ 同时处理成功和失败。
-
.catch(失败时做什么)- 专门处理失败或出错的情况(比如网络错误、数据错误等)。
-
.finally(最后总是做什么)- 不管成功还是失败,最后都会执行,适合做清理工作(比如关闭加载动画)。
比喻:
就像你点外卖:
.then= 外卖到了,开心吃饭。.catch= 外卖送错了或丢了,联系客服处理。.finally= 不管外卖到没到,你都把桌上的碗筷摆好/收走。
举个例子:
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
const randomNumber: number = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber);
} else {
reject(new Error('Random number is too small'));
}
}, 1000);
})
-
const promise: Promise
创建一个名为 promise 的常量,它的类型是 Promise,意味着这个 Promise 在成功时会得到一个数字(number)。 -
new Promise((resolve, reject) => { … })
创建 Promise 对象,需要传入一个函数(执行器)。这个函数会立即执行,它有两个参数:
resolve:调用后会把 Promise 状态变为 成功(fulfilled)
reject:调用后会把 Promise 状态变为 失败(rejected)
- setTimeout(() => { … }, 1000)
延迟 1 秒执行里面的代码,模拟异步操作(比如网络请求) - const randomNumber = Math.random()
生成一个 0 到 1 之间的随机数。 - if (randomNumber > 0.5)
如果随机数大于 0.5 → 调用 resolve(randomNumber),Promise 成功,把随机数传递出去。
如果随机数小于等于 0.5 → 调用 reject(new Error(…)),Promise 失败,抛出一个错误。
使用:
promise
.then(num => {
console.log("成功,数字是:", num);
})
.catch(err => {
console.error("失败了:", err.message);
})
.finally(() => {
console.log("无论成功失败,我都执行");
});
2)async/await
简单来说,async/await 是 Promise 的“舒适版写法”,让你写异步代码像写同步代码一样简单清晰。
比喻说明
想象一下,以前用 .then 处理异步就像是:
- 点外卖
- 然后(等外卖到了)吃饭
- 然后(吃完后)洗碗
用 async/await 后,你可以这样写代码:
async function 吃饭流程() {
let 外卖 = await 点外卖(); // 等外卖到了才继续
吃(外卖); // 吃饭
await 洗碗(); // 等洗完碗
}
代码看起来就像是一步一步执行的,但其实 await 后面的等待(比如等外卖、等洗碗)并不会阻塞整个程序。
关键点
-
async函数- 函数前面加
async,这个函数永远返回 Promise。
- 函数前面加
-
await关键字- 只能在
async函数里面用。 await后面跟一个 Promise,它会“暂停”在这里,等到这个 Promise 出结果(成功或失败)后才继续往下执行。- 如果 Promise 成功 →
await返回成功的值。 - 如果 Promise 失败 →
await会抛出错误,需要用try...catch捕获。
- 只能在
-
错误处理
- 可以用
try { ... } catch (err) { ... }包裹await,来捕获错误,就像处理同步代码错误一样。
- 可以用
例子对比
Promise 写法:
function 获取数据() {
return fetch('url')
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error('出错:', err));
}
async/await 写法:
async function 获取数据() {
try {
const response = await fetch('url');
const data = await response.json();
console.log(data);
} catch (err) {
console.error('出错:', err);
}
}
一句话总结
async/await 就是:
- 用同步的写法,写异步的逻辑
- 等 Promise 结果时,不阻塞程序其他部分
- 用
try-catch捕获错误,更直观
3、多线程并发
简单来说,ArkTS(鸿蒙应用开发语言)提供了两种处理“耗时任务不卡界面”的方式:TaskPool 和 Worker,它们都是为了让你的App更流畅。
比喻说明
想象你的App是一个餐厅:
- 主线程 = 前台服务员(直接跟顾客打交道)
- 耗时任务 = 做复杂菜品、处理大量食材
问题:如果服务员自己去做菜,前台就没人接待顾客了,餐厅会显得“卡住”。
解决方案:
-
请专门的厨师(Worker)
- 固定雇佣一个厨师专门做某类菜
- 适合:长期稳定的后台任务(如持续采集传感器数据)
-
用临时帮厨团队(TaskPool)
- 有个帮厨池,需要时叫一个临时帮厨来处理零活
- 适合:临时的大量计算(如图片处理、文件压缩)
两种方式对比
| 特点 | Worker(固定厨师) | TaskPool(帮厨团队) |
|---|---|---|
| 创建成本 | 较高(每个Worker都是独立线程) | 较低(线程池共享) |
| 适合场景 | 长期运行的任务 需要保持状态的任务 |
短期大量计算 频繁的临时任务 |
| 通信方式 | 通过消息传递(不会直接共享内存) | 通过消息传递 |
| 生命周期 | 需要手动创建和关闭 | 系统管理,自动调度 |
为什么用这种模型?
- 避免“锁”的麻烦:传统多线程要小心数据冲突,而Actor模型(消息传递)就像厨师和服务员通过“菜单纸条”沟通,不会互相干扰。
- 不卡界面:耗时任务都交给后台线程,前台操作依然流畅。
实际应用场景
-
图片处理(用TaskPool)
// 比如用户选择10张照片要加滤镜 // 用TaskPool同时处理多张,处理完一张更新一张预览 -
持续定位(用Worker)
// 导航App需要持续获取GPS位置 // 用一个Worker专门监听位置变化,不干扰主界面操作 -
游戏逻辑(Worker或TaskPool)
// 游戏中的AI计算、物理模拟等 // 可以放在后台线程,保证画面渲染流畅
一句话总结
ArkTS的多线程:
- Worker = 长期固定的后台助手
- TaskPool = 临时的任务处理小组
- 共同目标 = 让耗时任务在后台跑,主界面永远流畅响应
四、实践任务
1、装饰器实战
// 实现一个简单的状态管理装饰器
function MyDescriptor(target: Object, key: string, descriptor: PropertyDescriptor) {
const originalMethod: Function = descriptor.value
descriptor.value = (...args: Object[]) => {
// Get the name, input parameters, and return value of the decorated method
console.log(`Calling ${target.constructor?.name} method ${key} with argument: ${args}`)
const result: Object = originalMethod(...args)
console.log(`Method ${key} returned: ${result}`)
return result
}
return descriptor
}
@Entry
@Component
export struct MyDescriptorCom {
@State message: string = 'Hello World';
@MyDescriptor
demoFunc(str: string) {
return str
}
aboutToAppear(): void {
this.demoFunc('夏小鱼的装饰器')
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
// 运行后日志:
// Calling MyDescriptorCom method demoFunc with argument: 夏小鱼的装饰器
// Method demoFunc returned: 夏小鱼的装饰器
更多推荐



所有评论(0)