HarmonyOS ArkTS 倒计时组件实战:设计模式实践篇 - 标志位驱动渲染与状态机模式
本文是《HarmonyOS ArkTS 企业级倒计时组件设计与实现》系列的第三篇,将深入探讨标志位驱动渲染模式和状态机模式的实际应用。适合中级开发者和架构师,学习如何在实际项目中应用设计模式。
📋 前言
在第二篇文章中,我们讨论了过度设计的问题:AI 给出的是更贴近 GoF 的“上下文+选择器+策略对象”的重型策略实现,而我们的场景映射/Record 版本其实也是策略模式的轻量实现。这里继续延伸,探讨在倒计时组件中真正适用且足够轻量的模式:标志位驱动渲染和状态机模式(二者同样体现“条件→行为/显示”的策略思想,只是以数据/表驱动的形式落地)。
这两种模式在倒计时组件中发挥了重要作用,既保持了代码简洁,又提供了良好的可维护性。
🎯 标志位驱动渲染模式
什么是标志位驱动渲染?
标志位驱动渲染是一种通过布尔值标志位控制UI元素显示/隐藏的设计模式。在倒计时组件中,我们使用 Record<string, boolean> 来管理所有显示元素的标志位。
设计思路
倒计时组件需要显示多种时间格式,每种格式包含不同的元素:
- 数字:D、hh、mm、ss、ms
- 分隔符::1、:2、.
- 文字:天、小时
如果使用传统的 if-else 判断,代码会非常冗长:
// 传统方式(不推荐)
if (showDay) {
Text(this.D.toString())
}
if (showDay && showDayText) {
Text('天')
}
if (showHour) {
Text(this.hh.toString())
}
// ... 大量重复的if判断
使用标志位驱动,代码更简洁:
@Local timeFormatFlags: Record<string, boolean> = {
'D': false,
'天': false,
'hh': !this.shouldHideHour ?? false,
':1': !this.shouldHideHour ?? false,
'小时': false,
'mm': false,
':2': false,
'ss': false,
'.': this.shouldShowMS ?? false,
'ms': this.shouldShowMS ?? false,
}
build() {
Row() {
if (this.timeFormatFlags['D']) {
Text(this.D.toString())
}
if (this.timeFormatFlags['天']) {
Text('天')
}
if (this.timeFormatFlags['hh']) {
Text(this.padZero(this.hh))
}
// ... 统一的模式
}
}
核心实现
1. 标志位初始化
@Local timeFormatFlags: Record<string, boolean> = {
'自定义文本': this.activeCustomizedText ? true : false,
'D': false,
'天': false,
'hh': !this.shouldHideHour ?? false,
':1': !this.shouldHideHour ?? false,
'小时': false,
'mm': false,
':2': false,
'ss': false,
'.': this.shouldShowMS ?? false,
'ms': this.shouldShowMS ?? false,
}
设计要点:
- 使用字符串键,语义清晰
- 初始值根据配置设置
- 后续通过
assignFlags方法动态更新
2. 标志位更新方法
assignFlags(keyList: string) {
this.keyList = keyList // 保存格式方案
// 使用字符串中的键查询并开启布尔值
Object.keys(this.timeFormatFlags).forEach((key): boolean =>
(this.timeFormatFlags[key] = keyList.includes(key))
)
}
工作原理:
- 输入:格式字符串,如
'hh:1mm:2ss' - 处理:遍历所有标志位,检查是否在格式字符串中
- 输出:更新标志位,控制UI显示
示例:
// 输入:'hh:1mm:2ss'
// 处理后:
// timeFormatFlags['hh'] = true
// timeFormatFlags[':1'] = true
// timeFormatFlags['mm'] = true
// timeFormatFlags[':2'] = true
// timeFormatFlags['ss'] = true
// 其他 = false
3. UI渲染
build() {
Row() {
// 前缀
if (this.customStyleEnabled) {
Text(this.activePre)
.exNumber(this.fontSize, this.decorationFontColor, this.fontWeight)
}
// 自定义文本
if (this.timeFormatFlags['自定义文本']) {
Text(this.activeCustomizedText)
.exNumber(this.fontSize, this.customizedFontColor, this.fontWeight)
}
// 天
if (this.timeFormatFlags['D']) {
Text(this.D.toString())
.exNumber(this.fontSize, this.numberFontColor, this.fontWeight,
this.numberBgColor, this.numberBgRadius)
}
if (this.timeFormatFlags['天']) {
Text('天')
.exNumber(this.fontSize, this.separatorFontColor, this.fontWeight)
}
// 小时
if (this.timeFormatFlags['hh']) {
Text(this.padZero(this.hh))
.exNumber(this.fontSize, this.numberFontColor, this.fontWeight,
this.numberBgColor, this.numberBgRadius)
}
if (this.timeFormatFlags['小时']) {
Text('小时')
.exNumber(this.fontSize, this.separatorFontColor, this.fontWeight)
}
if (this.timeFormatFlags[':1']) {
Text(':')
.exNumber(this.fontSize, this.separatorFontColor, this.fontWeight)
}
// 分钟
if (this.timeFormatFlags['mm']) {
Text(this.padZero(this.mm))
.exNumber(this.fontSize, this.numberFontColor, this.fontWeight,
this.numberBgColor, this.numberBgRadius)
}
if (this.timeFormatFlags[':2']) {
Text(':')
.exNumber(this.fontSize, this.separatorFontColor, this.fontWeight)
}
// 秒和毫秒
Row() {
if (this.timeFormatFlags['ss']) {
Text(this.padZero(this.ss))
.exNumber(this.fontSize, this.numberFontColor, this.fontWeight)
}
if (this.timeFormatFlags['.']) {
Text('.')
.exNumber(this.fontSize, this.numberFontColor, this.fontWeight)
}
if (this.timeFormatFlags['ms']) {
Text(this.ms.toString())
.exNumber(this.fontSize, this.numberFontColor, this.fontWeight)
}
}
.borderRadius(this.numberBgRadius)
.backgroundColor(this.numberBgColor)
// 后缀
if (this.customStyleEnabled && this.activeSuf) {
Text(this.activeSuf)
.exNumber(this.fontSize, this.decorationFontColor, this.fontWeight)
}
}
.position({ top: this.y, left: this.x })
}
优势分析
1. 代码简洁性
对比传统方式:
// 传统方式:需要大量if-else判断
if (format === 'hh:mm:ss') {
if (showHour) {
Text(this.hh)
if (showSeparator) Text(':')
}
if (showMinute) {
Text(this.mm)
if (showSeparator) Text(':')
}
// ... 大量嵌套
}
标志位驱动:
// 统一模式,代码清晰
if (this.timeFormatFlags['hh']) {
Text(this.padZero(this.hh))
}
if (this.timeFormatFlags[':1']) {
Text(':')
}
2. 易于扩展
新增格式只需:
- 在
timeFormatFlags中添加新标志位(如需要) - 在
assignFlags中处理新格式字符串 - 在
build中添加对应的UI元素
无需修改大量if-else逻辑。
3. 可维护性
- 标志位集中管理,易于理解
- 格式字符串语义清晰(
'hh:1mm:2ss') - 修改格式只需更新标志位,不影响UI结构
与场景映射方案的对比
在第二篇文章中,我们使用了场景映射方案:
const occasions: Record<string, string> = {
'禁用自定义': above48h ? 'D天hh小时' : 'hh:1mm:2ss',
'启用自定义0': minuteForm,
// ...
}
两种方案的对比:
| 维度 | 标志位驱动 | 场景映射 |
|---|---|---|
| 适用场景 | UI元素多,需要精细控制 | 格式选择逻辑复杂 |
| 代码量 | 中等 | 较少 |
| 可读性 | 高(标志位语义清晰) | 高(场景映射直观) |
| 扩展性 | 高(添加标志位即可) | 高(添加场景即可) |
| 维护成本 | 中等 | 低 |
结论: 两种方案都是优秀的设计,选择取决于具体场景。在倒计时组件中,我们结合使用了两种方案:
- 场景映射:选择格式(
decideWhatToShow) - 标志位驱动:控制UI显示(
build)
🔄 状态机模式
什么是状态机?
状态机是一种管理对象状态转换的设计模式。在倒计时组件中,我们使用状态机管理定时器的状态。
状态定义
enum TimerStatus {
IDLE = 0, // 空闲:未启动或已停止
RUNNING = 1, // 运行中:正在倒计时
PAUSED = 2 // 暂停:已暂停,可以恢复
}
状态转换图
startCountdown()
IDLE ──────────────→ RUNNING
↑ │
│ │ pauseCountdown()
│ ↓
│ PAUSED
│ │
│ │ startCountdown()
│ ↓
└────────────────── RUNNING
stopCountdown()
状态转换实现
1. 启动倒计时
startCountdown(): void {
// 如果正在运行,直接返回(防止重复启动)
if (this.status === TimerStatus.RUNNING) {
return
}
// 状态转换:IDLE/PAUSED → RUNNING
this.status = TimerStatus.RUNNING
// 清除之前的定时器
if (this.timerId !== 0) {
clearInterval(this.timerId)
}
// 启动新定时器
this.timerId = setInterval(() => {
// ... 倒计时逻辑
}, this.interval)
}
关键点:
- 状态检查:防止重复启动
- 状态转换:明确的状态变更
- 资源管理:清理旧定时器
2. 暂停倒计时
pauseCountdown(): void {
// 只有运行中才能暂停
if (this.status === TimerStatus.RUNNING) {
// 状态转换:RUNNING → PAUSED
this.status = TimerStatus.PAUSED
// 清理定时器
if (this.timerId !== 0) {
clearInterval(this.timerId)
this.timerId = 0
}
}
// 刷新显示
this.refreshTimeNumber(this.remainingTime)
}
关键点:
- 状态检查:只有运行中才能暂停
- 状态转换:明确的状态变更
- 资源清理:释放定时器资源
3. 停止倒计时
stopCountdown(): void {
// 状态转换:任意状态 → IDLE
this.status = TimerStatus.IDLE
// 清理定时器
if (this.timerId !== 0) {
clearInterval(this.timerId)
this.timerId = 0
}
// 重置时间
this.remainingTime = 0
this.refreshTimeNumber(this.remainingTime)
}
状态机的优势
1. 状态管理清晰
使用枚举定义状态,避免魔法数字:
// 不好的做法
if (this.status === 1) { // 1是什么?
// ...
}
// 好的做法
if (this.status === TimerStatus.RUNNING) {
// ...
}
2. 防止状态混乱
通过状态检查,防止非法状态转换:
// 防止重复启动
if (this.status === TimerStatus.RUNNING) {
return
}
// 只有运行中才能暂停
if (this.status === TimerStatus.RUNNING) {
this.status = TimerStatus.PAUSED
}
3. 易于调试
状态明确,便于日志记录和调试:
Logger.info(TAG, `状态转换:${this.status} → ${TimerStatus.RUNNING}`)
实际应用场景
1. 页面生命周期管理
aboutToAppear(): void {
this.startCountdown() // IDLE → RUNNING
// ...
}
aboutToDisappear(): void {
this.stopCountdown() // RUNNING → IDLE
// ...
}
2. 页面显示/隐藏事件
if (!this.ignorePause) {
emitter.on(PAGE_SHOW_EVENT, () => {
this.startCountdown() // PAUSED → RUNNING
})
emitter.on(PAGE_HIDE_EVENT, () => {
this.pauseCountdown() // RUNNING → PAUSED
})
}
3. 倒计时结束
if (this.remainingTime <= 0) {
this.stopCountdown() // RUNNING → IDLE
this.timeOutAction()
}
🎯 设计模式的选择原则
什么时候用设计模式?
✅ 适合使用设计模式的场景
- 问题复杂度高:需要管理多个状态、多种策略
- 扩展需求明确:未来需要频繁扩展
- 代码重复多:有大量重复逻辑
- 维护成本高:当前代码难以维护
❌ 不适合使用“重型/OOP”实现的场景
- 问题简单:简单的 if-else 或轻量表驱动即可
- 扩展需求不明确:未来扩展可能性低
- 代码量增加明显:重型实现引入大量样板代码
- 过度设计:为了用设计模式而用设计模式
倒计时组件中的设计模式应用
| 设计模式 | 应用场景 | 是否适合 | 理由 |
|---|---|---|---|
| 标志位驱动 | UI元素显示控制 | ✅ 适合 | 元素多,需要精细控制 |
| 状态机 | 定时器状态管理 | ✅ 适合 | 状态转换复杂,需要清晰管理 |
| 策略模式(轻量/表驱动) | 格式选择逻辑 | ✅ 适合 | 逻辑简单,用场景映射/Record 轻量实现 |
| 策略模式(重型/OOP) | 同上 | ❌ 不适合当前场景 | 场景简单,重型实现代码量大、收益低 |
与第二篇的呼应
- 第二篇结论:AI 给出的 GoF/OOP 重型策略在本场景“成本过高”;场景映射/Record 版本仍然是策略模式,但更轻量。
- 本文延伸:
- 标志位驱动:解决 UI 元素多、精细控制的问题,轻量且清晰。
- 状态机:解决状态转换复杂的问题,逻辑清晰。
- 轻量策略/表驱动:格式选择逻辑用场景映射实现策略意图,而非 OOP 仪式。
关键区别:
- 重型/OOP 策略:为“优雅/形式”增加复杂度,当前场景收益低。
- 轻量策略(场景映射/表驱动)、标志位驱动、状态机:为解决实际问题而用,成本低、收益高。
📊 设计模式对比分析
标志位驱动 vs 传统if-else
| 维度 | 传统if-else | 标志位驱动 |
|---|---|---|
| 代码量 | 多(大量嵌套) | 少(统一模式) |
| 可读性 | 低(嵌套深) | 高(语义清晰) |
| 扩展性 | 低(需要修改多处) | 高(添加标志位) |
| 维护性 | 低(逻辑分散) | 高(集中管理) |
状态机 vs 布尔标志
| 维度 | 布尔标志 | 状态机 |
|---|---|---|
| 状态数量 | 适合2-3个状态 | 适合3+个状态 |
| 状态转换 | 不清晰 | 清晰 |
| 防止错误 | 弱 | 强(状态检查) |
| 可维护性 | 中等 | 高 |
💡 最佳实践
1. 标志位驱动的实践
- ✅ 使用语义化的键名(
'hh'、':1'而不是'flag1'、'flag2') - ✅ 集中管理标志位(使用
Record<string, boolean>) - ✅ 通过字符串模式更新标志位(
assignFlags) - ❌ 避免在多个地方直接修改标志位
2. 状态机的实践
- ✅ 使用枚举定义状态(避免魔法数字)
- ✅ 明确状态转换条件(在方法中检查)
- ✅ 记录状态转换日志(便于调试)
- ❌ 避免跳过状态检查(防止状态混乱)
3. 设计模式的选择
- ✅ 先分析问题,再选择模式
- ✅ 评估代码量增加是否值得
- ✅ 考虑未来扩展需求
- ❌ 不要为了用设计模式而用设计模式
🎓 总结
本文探讨了倒计时组件中真正适用的设计模式:
- 标志位驱动渲染:解决了UI元素多、需要精细控制的问题
- 状态机模式:解决了状态管理复杂、需要清晰转换的问题
这两种模式都:
- ✅ 解决了实际问题
- ✅ 代码简洁清晰
- ✅ 易于维护扩展
- ✅ 没有过度设计
与第二篇文章形成呼应:不是所有设计模式都适合,要选择真正解决问题的模式。
在下一篇文章中,我们将探讨性能优化的实战技巧,包括渲染优化、定时器优化和内存管理。
系列文章导航:
- [第1篇] 基础篇:从需求分析到基础实现
- [第2篇] 设计模式反思篇:当AI建议用策略模式时,我选择了质疑
- [第3篇] 设计模式实践篇:标志位驱动渲染与状态机模式(本文)
- [第4篇] 性能优化篇:从100ms刷新到流畅体验
- [第5篇] 高级特性篇:时间区间样式切换
- [第6篇] 工程实践篇:测试与质量保证
- [第7篇] 总结篇:最佳实践与思考
讨论: 你在项目中是如何选择设计模式的?欢迎在评论区分享你的经验!