前置知识: HarmonyOS

自定义组件

54 minIntermediate2026/6/14

自定义组件与生命周期

自定义组件:ArkUI 声明式组件模型与组合复用工程实践

组件是声明式 UI 的”原子单元”——每个组件既是渲染输出单位,也是状态隔离与复用的边界。本章按 CMU 15-410(Distributed Systems)、MIT 6.5840(Distributed Systems)、Stanford CS142(Web Applications)等课程标准组织,系统讲解 ArkUI 自定义组件的声明模型、生命周期回调矩阵、@Component/@Entry/@Builder/@BuilderParam/@Styles/@Extend/@Reusable 装饰器体系、组件间通信(@Prop/@Link/@Provide/@Consume/@ObjectLink/@Watch/事件回调)、插槽机制、样式复用、组件复用池、性能优化(延迟加载、按需构建、VSync 同步)、跨组件状态同步、组件库设计模式等核心议题,并对照 React Component、Vue SFC、SwiftUI View、Jetpack Compose Composable 等业界方案。


1. 学习目标

本章按照 Bloom 教育目标分类法(Bloom’s Taxonomy)的六个层级组织学习目标。读者完成本章后应能够:

1.1 Remember(记忆)

  • R1:复述 ArkUI 自定义组件的六个生命周期回调:aboutToAppearaboutToDisappearonPageShowonPageHideonBackPressaboutToReuse@Reusable 专用)。
  • R2:列举 ArkUI 组件通信装饰器矩阵:@Prop(单向)、@Link(双向)、@Provide/@Consume(跨层级)、@ObjectLink(嵌套对象)、@Watch(监听)、@StorageLink/@LocalStorageLink(全局/局部存储)。
  • R3:复述 @Builder@BuilderParam 的区别:前者用于声明可复用 UI 片段,后者用于声明组件插槽。
  • R4:复述 @Styles@Extend 的区别:前者仅可复用通用样式属性,后者可扩展原生组件方法并支持参数。
  • R5:复述 @Reusable 装饰器的工作机制:组件实例进入复用池后保留状态,下次同类型节点出现时复用而非销毁重建。
  • R6:复述 @Component@Entry 的关系:@Entry 标记页面根组件,绑定路由;@Component 仅标记可复用组件。

1.2 Understand(理解)

  • U1:阐明 ArkUI 自定义组件与 React Function Component、Vue SFC、SwiftUI View、Compose Composable 的本质差异:ArkUI 基于 struct + 装饰器,编译期生成状态追踪代码。
  • U2:解释 @Prop 单向数据流与 @Link 双向绑定的实现差异:@Prop 在子组件内部做值拷贝,@Link 建立父子状态对象的引用关系。
  • U3:解释 @BuilderParam 的”插槽”机制与 React children/Vue <slot>/Web Components <slot> 的等价性。
  • U4:对比 @Styles@Extend@Styles 是属性集合的复用,@Extend 是方法扩展,两者本质是编译期宏展开。
  • U5:解释 @Reusable 复用池的”LRU + 类型匹配”策略:仅当新节点与池中节点类型、@Reusable id 一致时才命中。
  • U6:阐明 aboutToAppearbuild() 的执行顺序:前者在组件创建后、首次 build() 前执行,用于异步数据初始化。

1.3 Apply(应用)

  • A1:使用 @Component + @Prop + @BuilderParam 实现一个带插槽的卡片组件,支持自定义头部与内容。
  • A2:使用 @Reusable + aboutToReuse 实现一个列表项复用组件,避免滑动时的重建开销。
  • A3:使用 @Styles 提取通用按钮样式,使用 @ExtendText 扩展 titleStyle() 方法。
  • A4:使用 @Provide/@Consume 实现一个主题切换功能,根组件提供 theme,深层叶子组件消费。
  • A5:使用 @Watch 实现一个监听器,当 @State 变化时触发副作用(如埋点、本地存储)。

1.4 Analyze(分析)

  • An1:分析 ArkUI 选择 struct + 装饰器而非 class 继承的工程动机:避免继承层次过深、便于 AOT 静态分析。
  • An2:分析 @Reusable 复用池的命中条件与未命中时的退化路径:未命中时走标准创建流程。
  • An3:分析 @BuilderParam 多插槽场景下的冲突解决:按声明顺序匹配,或通过命名插槽显式指定。
  • An4:分析 @Prop 深拷贝在大对象场景下的性能成本,给出使用 @Link@ObjectLink 的替代方案。

1.5 Evaluate(评价)

  • E1:评价 ArkUI 的”struct + 装饰器”模型相比 React Hooks 的优劣:编译期优化 vs 运行时灵活性。
  • E2:评价 @Reusable 复用机制相比 React memo、Vue keep-alive 的设计差异与适用场景。
  • E3:评价 @Styles/@Extend 作为编译期宏展开相比 Tailwind 工具类、CSS-in-JS 的工程价值。

1.6 Create(创造)

  • C1:设计一个企业级组件库的基础设施:包含设计令牌(Design Token)、主题切换、国际化、无障碍、按需加载。
  • C2:设计一个跨组件状态同步中间件:基于 @Provide/@Consume + AppStorage 实现全局状态与局部状态的协调。
  • C3:设计一个组件性能监控 SDK:自动采集 build() 耗时、@Reusable 命中率、@Watch 触发频率,输出火焰图。

2. 历史动机与发展脉络

2.1 声明式 UI 的演进(2014-2020)

声明式 UI 范式经历了从虚拟 DOM 到编译期生成代码的转变:

年代框架模型关键特征
2014React 0.13Function Component + JSX运行时 Virtual DOM Diff
2016Vue 2.0SFC + Options API响应式 + 模板编译
2018SwiftUI 1.0struct View + ViewBuilder编译期生成渲染树
2019Jetpack Compose 1.0@Composable + Kotlin Compiler编译期插桩 + Positional Memoization
2020Flutter 1.0Widget + Element + RenderObject三树模型
2021ArkUI 1.0(HarmonyOS 3.0)struct + @Component + ArkTS编译期 AOT + 装饰器元编程

ArkUI 在设计上吸收了 SwiftUI 的 struct 模型与 Compose 的编译期插桩思想,但用装饰器(Decorator)替代了 Kotlin 的 @Composable 注解,使 TypeScript 开发者更易接受。

2.2 ArkUI 1.0(2021):基础组件模型

HarmonyOS 3.0 首次推出 ArkUI 1.0:

  • 引入 @Component@Entry@State@Prop@Link@Provide@Consume@Builder@BuilderParam@Styles@Extend 等装饰器。
  • 组件为 struct,build() 方法返回 UI 树。
  • 生命周期仅 aboutToAppearaboutToDisappearonPageShowonPageHideonBackPress
  • 无组件复用机制,列表滑动时频繁创建销毁。

2.3 ArkUI 2.0(2022):状态管理增强

HarmonyOS 3.1 引入 ArkUI 2.0:

  • 引入 @Watch 装饰器,支持监听状态变化。
  • 引入 @ObjectLink + @Observed,支持嵌套对象的响应式。
  • 引入 AppStorageLocalStorage,实现全局与局部状态存储。
  • 引入 @StorageLink@StorageProp@LocalStorageLink@LocalStorageProp
  • 组件间通信能力完善,但仍无复用机制。

2.4 ArkUI 3.0(2023):复用与性能

HarmonyOS 4.0 引入 ArkUI 3.0:

  • 引入 @Reusable 装饰器与 aboutToReuse 生命周期,实现组件复用池。
  • 引入 LazyForEach 懒加载,配合 @Reusable 优化长列表性能。
  • 引入 @AnimatableExtend,支持自定义动画属性。
  • 编译器优化:build() 方法编译期生成 RenderFunction,减少运行时开销。
  • 引入 if/elseForEach 的条件渲染追踪,避免无效 diff。

2.5 ArkUI 4.0(2024):跨端与原子化

HarmonyOS NEXT 引入 ArkUI 4.0:

  • 跨端组件模型:同一组件可在手机、平板、手表、车机上自适应渲染。
  • 原子化服务组件:@AtomicService 装饰器,标记可免安装运行的组件。
  • 引入 @ComponentV2(实验性):基于新的状态管理模型,支持更细粒度的依赖追踪。
  • 引入 @Param/@Once/@Event 等新装饰器,与 @ComponentV2 配套。
  • 性能:编译期生成 .abc 文件,build() 耗时降低 40%。

2.6 OpenHarmony ArkUI 演进

OpenHarmony 版本ArkUI 版本关键特性
3.01.0基础装饰器矩阵
3.22.0@Watch、@ObjectLink、AppStorage
4.03.0@Reusable、LazyForEach 优化
5.04.0@ComponentV2、跨端组件

2.7 时间线总览

2021 ──── ArkUI 1.0 ──── 基础装饰器矩阵(@Component/@State/@Prop/@Link)
2022 ──── ArkUI 2.0 ──── 状态管理增强(@Watch/@ObjectLink/AppStorage)
2023 ──── ArkUI 3.0 ──── 复用与性能(@Reusable/LazyForEach)
2024 ──── ArkUI 4.0 ──── 跨端与原子化(@ComponentV2/@AtomicService)

3. 形式化定义

3.1 自定义组件的形式化定义

定义 ArkUI 自定义组件为七元组:

C=S,P,L,B,R,E,M\mathcal{C} = \langle \mathcal{S}, \mathcal{P}, \mathcal{L}, \mathcal{B}, \mathcal{R}, \mathcal{E}, \mathcal{M} \rangle

其中:

  • S:State\mathcal{S}: \text{State} 为组件内部状态集合,由 @State@Provide 等装饰器声明。
  • P:Props\mathcal{P}: \text{Props} 为组件输入属性集合,由 @Prop@Link@BuilderParam 等声明。
  • L:Lifecycle\mathcal{L}: \text{Lifecycle} 为生命周期回调集合,包含 aboutToAppearaboutToDisappear 等。
  • B:BuildContextUITree\mathcal{B}: \text{BuildContext} \to \text{UITree} 为构建函数,输入构建上下文,输出 UI 树。
  • R:ReusePool\mathcal{R}: \text{ReusePool} 为复用池(仅 @Reusable 组件),存储可复用的组件实例。
  • E:EventStream\mathcal{E}: \text{EventStream} 为组件发出的事件流,通过回调函数向父组件传递。
  • M:Metadata\mathcal{M}: \text{Metadata} 为组件元数据,包含组件名、参数类型、装饰器信息等,由编译期生成。

3.2 组件实例的生命周期

组件实例 cc 的生命周期状态转换:

State(c){Created,Appearing,Appeared,Disposing,Disposed,Recycled}\text{State}(c) \in \{\text{Created}, \text{Appearing}, \text{Appeared}, \text{Disposing}, \text{Disposed}, \text{Recycled}\}

状态转移:

CreatedaboutToAppearAppearingAppearingbuild()AppearedAppearedonPageShowAppearedAppearedonPageHideAppearedAppearedremove from treeDisposingDisposingaboutToDisappearDisposedAppeared@Reusable recycleRecycledRecycledaboutToReuseAppeared\begin{aligned} \text{Created} &\xrightarrow{\text{aboutToAppear}} \text{Appearing} \\ \text{Appearing} &\xrightarrow{\text{build()}} \text{Appeared} \\ \text{Appeared} &\xrightarrow{\text{onPageShow}} \text{Appeared} \\ \text{Appeared} &\xrightarrow{\text{onPageHide}} \text{Appeared} \\ \text{Appeared} &\xrightarrow{\text{remove from tree}} \text{Disposing} \\ \text{Disposing} &\xrightarrow{\text{aboutToDisappear}} \text{Disposed} \\ \text{Appeared} &\xrightarrow{\text{@Reusable recycle}} \text{Recycled} \\ \text{Recycled} &\xrightarrow{\text{aboutToReuse}} \text{Appeared} \end{aligned}

3.3 单向数据流的形式化

@Prop 单向数据流的语义:

Parent(p)@PropChild(c)    c.p=clone(p)¬(cp)\text{Parent}(p) \xrightarrow{\text{@Prop}} \text{Child}(c) \implies c.p = \text{clone}(p) \wedge \neg(c \to p)

子组件对 @Prop 的修改仅影响本地拷贝,不回传父组件。当父组件 pp 变化时,触发子组件 cc 的重新构建:

Δp    Rebuild(c)    c.pclone(p)\Delta p \implies \text{Rebuild}(c) \implies c.p \leftarrow \text{clone}(p')

3.4 双向绑定的形式化

@Link 双向绑定的语义:

Parent(p)@LinkChild(c)    c.pp(cp)(pc)\text{Parent}(p) \xleftrightarrow{\text{@Link}} \text{Child}(c) \implies c.p \equiv p \wedge (c \to p) \wedge (p \to c)

任一方的修改都会同步到另一方。实现上,@Link 建立的是对父组件状态对象的”引用”(通过 ES Proxy 或 ArkUI 内部状态对象),而非值拷贝。

3.5 跨层级传递的形式化

@Provide/@Consume 的语义:

Ancestor(a)@ProvideDescendant(d)@ConsumeValue\text{Ancestor}(a) \xrightarrow{\text{@Provide}} \text{Descendant}(d) \xrightarrow{\text{@Consume}} \text{Value}

祖先组件 aa 通过 @Provide 声明一个”注入值”,后代组件 dd 通过 @Consume “消费”该值。注入与消费通过 key 匹配,key 可为字符串或 symbol。

Provide(a,k,v)    dDescendants(a):Consume(d,k)=v\text{Provide}(a, k, v) \implies \forall d \in \text{Descendants}(a): \text{Consume}(d, k) = v

3.6 复用池的形式化

@Reusable 复用池的工作机制:

Recycle(c)    Pool[type(c)].push(c)\text{Recycle}(c) \implies \text{Pool}[\text{type}(c)].\text{push}(c)

当新的同类型组件 cc' 需要创建时:

Reuse(c)    cPool[type(c)]:ccaboutToReuse(c,newParams)\text{Reuse}(c') \implies \exists c \in \text{Pool}[\text{type}(c')]: c' \leftarrow c \wedge \text{aboutToReuse}(c', \text{newParams})

复用池采用 LRU(Least Recently Used)策略,容量上限默认为 128 个实例。

3.7 构建函数的纯函数性

理想情况下,build() 应为纯函数:

build:State×PropsUITree\text{build}: \text{State} \times \text{Props} \to \text{UITree}

即给定相同的状态与属性,应输出相同的 UI 树。但实际上,build() 中可包含副作用(如日志、埋点),ArkUI 不强制纯函数性,但建议遵循单向数据流原则。

3.8 依赖追踪的复杂度

ArkUI 的响应式系统对 @State 的依赖追踪复杂度:

  • 首次构建O(n)O(n)nn 为 build() 中读取的 @State 数量。
  • 后续更新O(k)O(k)kk 为变化的 @State 数量,仅触发依赖该状态的子树重建。
RebuildCost(Δs)=cDependents(Δs)BuildCost(c)\text{RebuildCost}(\Delta s) = \sum_{c \in \text{Dependents}(\Delta s)} \text{BuildCost}(c)

4. 理论推导与原理解析

4.1 声明式 UI 的核心不变量

声明式 UI 的核心不变量是”UI 是状态的函数”:

UI=f(State)\text{UI} = f(\text{State})

ArkUI 通过装饰器在编译期插桩,将 @State 的读写转化为带依赖追踪的操作:

// 源码
@State count: number = 0;
build() {
  Text(`${this.count}`)
}

// 编译后(伪代码)
private __count: ObservedProperty<number> = new ObservedProperty(0);
get count() { return this.__count.get(); }
set count(v) { this.__count.set(v); }
build() {
  Text(`${this.__count.trackRead()}`); // trackRead 注册依赖
}

这种编译期变换使开发者无需手动管理依赖,但也意味着 @State 必须通过 this. 访问才能触发追踪。

4.2 @Prop 深拷贝的成本分析

@Prop 在子组件内部做值拷贝,对于基本类型(number、string、boolean)成本为 O(1)O(1),但对于对象数组成本为 O(n)O(n)

Cost(@Prop)={O(1)if primitiveO(n)if array of size nO(d)if object of depth d\text{Cost}(\text{@Prop}) = \begin{cases} O(1) & \text{if primitive} \\ O(n) & \text{if array of size } n \\ O(d) & \text{if object of depth } d \end{cases}

对于大对象(如 1000 条列表数据),使用 @Prop 会导致每次父组件更新时子组件深拷贝 1000 次。替代方案:

  • 使用 @Link 建立引用关系,避免拷贝。
  • 使用 @ObjectLink + @Observed,仅追踪变化的字段。
  • 使用 @Prop + @Watch,仅在变化时处理,但拷贝仍发生。

4.3 @BuilderParam 插槽的编译期展开

@BuilderParam 在编译期展开为函数参数:

// 源码
@Component
struct Card {
  @BuilderParam content: () => void;
  build() {
    Column() { this.content(); }
  }
}

// 使用
Card() {
  Text('Hello');
}

// 编译后(伪代码)
struct Card {
  content: () => void;
  build() {
    Column() { this.content(); }
  }
}
// 调用处
Card({ content: () => { Text('Hello'); } });

这种展开使插槽成为一等公民,可被传递、组合、缓存。

4.4 @Reusable 复用池的命中条件

@Reusable 复用池的命中条件:

Hit(c)    cPool:type(c)=type(c)reuseId(c)=reuseId(c)\text{Hit}(c') \iff \exists c \in \text{Pool}: \text{type}(c) = \text{type}(c') \wedge \text{reuseId}(c) = \text{reuseId}(c')

其中 reuseId 可通过 @Reusable({ reuseId: 'xxx' }) 显式指定,默认为组件类型名。

未命中时的退化路径:走标准创建流程,无性能损失。

4.5 @Styles 与 @Extend 的编译期宏展开

@Styles@Extend 在编译期展开为样式对象:

// 源码
@Styles function cardStyle() {
  .padding(16)
  .borderRadius(12)
  .backgroundColor(Color.White)
}

// 使用
Column() { ... }.cardStyle()

// 编译后(伪代码)
const cardStyle = { padding: 16, borderRadius: 12, backgroundColor: Color.White };
Column().applyStyle(cardStyle);

@Extend 类似,但可接受参数:

@Extend(Text) function title(size: number) {
  .fontSize(size).fontWeight(FontWeight.Bold)
}
// 编译后
function title(text: Text, size: number) {
  text.fontSize(size).fontWeight(FontWeight.Bold);
}

4.6 组件树与渲染树的分离

ArkUI 维护三棵树:

  • 组件树(Component Tree):开发者声明的 struct 树,对应源码结构。
  • 元素树(Element Tree):ArkUI 内部的元素实例树,维护状态与生命周期。
  • 渲染树(Render Tree):底层的渲染节点树,由 C++ 渲染引擎消费。
源码 build()    →    Component Tree (声明)
                          ↓ create
                     Element Tree (实例 + 状态)
                          ↓ diff
                     Render Tree (绘制)
                          ↓ paint
                     Screen

状态变化时,仅触发 Element Tree 的局部 diff 与 Render Tree 的局部更新,避免整树重建。

4.7 ForEach 的 key 机制

ForEachkeyGenerator 决定列表项的复用:

ForEach(arr, (item) => { Item({ item }) }, (item) => item.id)
  • key 稳定:列表项重排时,Element 复用,仅调整位置,性能 O(n)O(n)
  • key 不稳定(如用 index):列表项重排时,Element 销毁重建,性能 O(nlogn)O(n \log n)

形式化:

Reuse(ForEach)    key(old[i])=key(new[j])\text{Reuse}(\text{ForEach}) \iff \text{key}(\text{old}[i]) = \text{key}(\text{new}[j])

4.8 条件渲染的依赖追踪

if/else 分支在 ArkUI 中会被追踪:

if (this.isLoading) {
  Loading();
} else {
  Content();
}
  • 首次构建:根据 isLoading 选择分支,注册依赖。
  • isLoading 变化:销毁旧分支 Element,创建新分支 Element。

这意味着 if/else 切换是有成本的,频繁切换会导致 Element 频繁创建销毁。替代方案:使用 visibility 属性隐藏,保留 Element。

4.9 组件复用与内存占用

@Reusable 复用池的内存占用:

Memory(Pool)=cPoolSize(c)\text{Memory}(\text{Pool}) = \sum_{c \in \text{Pool}} \text{Size}(c)

复用池默认容量 128,对于大组件(如带图片的卡片),可能占用数十 MB。需通过 reuseMaxLimit 控制容量。

4.10 装饰器的编译期元编程

ArkUI 装饰器本质是 TypeScript 装饰器的扩展,在编译期由 ArkTS 编译器(基于 TypeScript Compiler API)变换 AST:

// 装饰器源码
@State count: number = 0;

// AST 变换后
private __count: ObservedProperty<number> = new ObservedProperty(0, this, 'count');
get count(): number { return this.__count.get(); }
set count(value: number) { this.__count.set(value); }

这种变换在编译期完成,运行时无反射开销,是 ArkUI 性能优于 React/Vue 的关键。


5. 代码示例

5.1 基础自定义组件:带插槽的卡片

// 通用卡片组件,支持头部、内容、底部三插槽
@Component
export struct Card {
  // 单向属性:标题、副标题
  @Prop title: string = '';
  @Prop subtitle: string = '';
  // 是否显示右上角操作按钮
  @Prop showAction: boolean = false;
  // 事件回调:点击操作按钮
  onAction?: () => void;
  // 插槽:自定义内容
  @BuilderParam content: () => void;
  // 插槽:自定义底部(可选)
  @BuilderParam footer?: () => void;

  // 组件即将出现:可在此初始化数据
  aboutToAppear() {
    console.info(`[Card] aboutToAppear, title=${this.title}`);
  }

  // 组件即将销毁:可在此清理资源
  aboutToDisappear() {
    console.info(`[Card] aboutToDisappear, title=${this.title}`);
  }

  build() {
    Column() {
      // 卡片头部
      Row() {
        Column() {
          Text(this.title)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1a1a1a');
          if (this.subtitle) {
            Text(this.subtitle)
              .fontSize(14)
              .fontColor('#999')
              .margin({ top: 4 });
          }
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start);

        if (this.showAction) {
          Text('更多')
            .fontSize(14)
            .fontColor('#007DFF')
            .onClick(() => this.onAction?.());
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16 });

      // 卡片内容(插槽)
      Column() {
        this.content();
      }
      .width('100%')
      .padding(16);

      // 卡片底部(可选插槽)
      if (this.footer) {
        Divider().color('#f0f0f0');
        Row() {
          this.footer();
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 });
      }
    }
    .width('100%')
    .borderRadius(12)
    .backgroundColor(Color.White)
    .shadow({ radius: 8, color: '#1A000000', offsetY: 2 });
  }
}

// 使用示例
@Entry
@Component
struct CardDemo {
  @State count: number = 0;

  build() {
    Column() {
      Card({ title: '用户信息', subtitle: '基本信息' }) {
        Column() {
          Text(`姓名:张三`);
          Text(`年龄:${this.count}`);
          Button('增加年龄')
            .onClick(() => this.count++);
        }
        .alignItems(HorizontalAlign.Start);
      }

      Card({ title: '订单列表', showAction: true, onAction: () => {
        console.info('查看更多订单');
      } }) {
        Column() {
          Text('订单1:¥99');
          Text('订单2:¥199');
        }
        .alignItems(HorizontalAlign.Start);
      } footer: {
        Button('查看全部').fontSize(14);
      }
    }
    .padding(16);
  }
}

5.2 带生命周期的定时器组件

@Component
struct Timer {
  // 父组件传入的总时长
  @Prop duration: number = 60;
  // 剩余时长(内部状态)
  @State remaining: number = 60;
  // 定时器句柄
  private intervalId: number = -1;

  // 组件即将出现:初始化并启动定时器
  aboutToAppear() {
    this.remaining = this.duration;
    this.intervalId = setInterval(() => {
      if (this.remaining > 0) {
        this.remaining--;
      } else {
        clearInterval(this.intervalId);
        this.intervalId = -1;
      }
    }, 1000);
  }

  // 组件即将销毁:清理定时器,避免内存泄漏
  aboutToDisappear() {
    if (this.intervalId !== -1) {
      clearInterval(this.intervalId);
      this.intervalId = -1;
    }
  }

  // 页面隐藏时暂停定时器,节省电量
  onPageHide() {
    if (this.intervalId !== -1) {
      clearInterval(this.intervalId);
      this.intervalId = -1;
    }
  }

  // 页面显示时恢复定时器
  onPageShow() {
    if (this.intervalId === -1 && this.remaining > 0) {
      this.intervalId = setInterval(() => {
        if (this.remaining > 0) {
          this.remaining--;
        } else {
          clearInterval(this.intervalId);
        }
      }, 1000);
    }
  }

  build() {
    Text(`${this.remaining}s`)
      .fontSize(40)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.remaining <= 10 ? '#FF4444' : '#1a1a1a');
  }
}

5.3 @Prop 单向数据流:用户卡片

interface UserProfile {
  id: number;
  name: string;
  avatar: string;
  level: number;
}

@Component
struct UserCard {
  // @Prop 单向:父组件更新时子组件重新构建
  @Prop user: UserProfile;
  // 点击事件回调
  onUserClick?: (id: number) => void;

  build() {
    Row() {
      Image(this.user.avatar)
        .width(60).height(60).borderRadius(30);
      Column() {
        Text(this.user.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium);
        Text(`Lv.${this.user.level}`)
          .fontSize(14)
          .fontColor('#FFA500')
          .margin({ top: 4 });
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12 });
    }
    .width('100%')
    .padding(16)
    .onClick(() => this.onUserClick?.(this.user.id));
  }
}

@Entry
@Component
struct UserList {
  @State users: UserProfile[] = [
    { id: 1, name: '张三', avatar: 'https://example.com/1.png', level: 5 },
    { id: 2, name: '李四', avatar: 'https://example.com/2.png', level: 10 },
  ];

  build() {
    Column() {
      ForEach(this.users, (user: UserProfile) => {
        UserCard({ user, onUserClick: (id) => {
          console.info(`点击用户 ${id}`);
        } });
      }, (user: UserProfile) => user.id.toString());
    }
    .padding(16);
  }
}
@Component
struct Toggle {
  // @Link 双向:父子同步
  @Link isOn: boolean;
  // 标签
  @Prop label: string = '';

  build() {
    Row() {
      Text(this.label).fontSize(16).layoutWeight(1);
      // 点击切换状态,@Link 自动同步到父组件
      Text(this.isOn ? 'ON' : 'OFF')
        .fontSize(16)
        .fontColor(this.isOn ? '#4CAF50' : '#999')
        .onClick(() => this.isOn = !this.isOn);
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.isOn ? '#E8F5E9' : '#F5F5F5')
    .borderRadius(8);
  }
}

@Entry
@Component
struct SettingsPage {
  @State wifiOn: boolean = false;
  @State bluetoothOn: boolean = true;

  build() {
    Column({ space: 12 }) {
      Text('设置').fontSize(24).fontWeight(FontWeight.Bold);
      // $ 语法建立双向绑定
      Toggle({ label: 'WiFi', isOn: $wifiOn });
      Toggle({ label: '蓝牙', isOn: $bluetoothOn });
      Text(`当前状态:WiFi=${this.wifiOn}, 蓝牙=${this.bluetoothOn}`)
        .fontSize(14)
        .fontColor('#666');
    }
    .padding(16);
  }
}

5.5 @Provide/@Consume 跨层级:主题切换

interface Theme {
  primary: string;
  background: string;
  text: string;
}

const lightTheme: Theme = {
  primary: '#007DFF',
  background: '#FFFFFF',
  text: '#1a1a1a',
};

const darkTheme: Theme = {
  primary: '#0A84FF',
  background: '#1C1C1E',
  text: '#FFFFFF',
};

// 根组件提供主题
@Entry
@Component
struct ThemeApp {
  @State isDark: boolean = false;
  // @Provide 向所有后代注入 theme
  @Provide('theme') theme: Theme = lightTheme;

  build() {
    Column() {
      Row() {
        Text('主题切换').fontSize(18).fontColor(this.theme.text);
        Toggle({ label: '深色', isOn: $isDark });
      }
      .padding(16);

      // 深层子组件
      ContentArea();
    }
    .backgroundColor(this.theme.background)
    .onClick(() => {
      // 切换主题,@Provide 自动通知所有 @Consume
      this.theme = this.isDark ? darkTheme : lightTheme;
    });
  }
}

// 中间层组件:无需接收 theme
@Component
struct ContentArea {
  build() {
    Column() {
      Card();
      Button('按钮');
    }
    .padding(16);
  }
}

// 叶子组件:消费主题
@Component
struct Card {
  // @Consume 消费祖先注入的 theme
  @Consume('theme') theme: Theme;

  build() {
    Column() {
      Text('卡片标题')
        .fontSize(18)
        .fontColor(this.theme.text);
      Text('卡片内容')
        .fontSize(14)
        .fontColor(this.theme.text);
    }
    .padding(16)
    .backgroundColor(this.theme.background)
    .borderRadius(12);
  }
}

@Component
struct Button {
  @Consume('theme') theme: Theme;

  build() {
    Text('按钮')
      .fontSize(16)
      .fontColor('#FFFFFF')
      .backgroundColor(this.theme.primary)
      .padding({ left: 24, right: 24, top: 12, bottom: 12 })
      .borderRadius(24);
  }
}

5.6 @Builder 与 @BuilderParam:可复用 UI 片段

// 全局 @Builder:可在任意组件中复用
@Builder
function IconText(icon: Resource, text: string, color: string) {
  Row() {
    Image(icon).width(20).height(20);
    Text(text).fontSize(14).fontColor(color).margin({ left: 8 });
  }
}

// 组件内 @Builder
@Component
struct ListItem {
  @Prop title: string;
  @Prop subtitle: string;
  // 插槽
  @BuilderParam trailing: () => void;

  // 组件内 @Builder:复用样式
  @Builder
  titleText() {
    Text(this.title)
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .maxLines(1);
  }

  build() {
    Row() {
      Column() {
        this.titleText();
        Text(this.subtitle)
          .fontSize(12)
          .fontColor('#999')
          .margin({ top: 4 });
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1);

      // 使用插槽
      this.trailing();
    }
    .width('100%')
    .padding(16);
  }
}

@Entry
@Component
struct ListPage {
  build() {
    Column({ space: 8 }) {
      ListItem({ title: '设置', subtitle: '应用设置' }) trailing: {
        IconText($r('app.media.arrow'), '>', '#CCC');
      };
      ListItem({ title: '通知', subtitle: '3 条未读' }) trailing: {
        Text('3').fontSize(12).fontColor('#FF4444');
      };
    }
    .padding(16);
  }
}

5.7 @Styles 与 @Extend:样式复用

// @Styles:复用通用样式属性
@Styles
function cardPadding() {
  .padding({ left: 16, right: 16, top: 12, bottom: 12 });
}

@Styles
function cardShadow() {
  .shadow({ radius: 8, color: '#1A000000', offsetY: 2 });
}

// @Extend:扩展 Text 组件,支持参数
@Extend(Text)
function titleStyle(size: number = 18) {
  .fontSize(size)
    .fontWeight(FontWeight.Bold)
    .fontColor('#1a1a1a')
    .maxLines(1);
}

// @Extend:扩展 Button 组件
@Extend(Button)
function primaryStyle() {
  .backgroundColor('#007DFF')
    .fontColor(Color.White)
    .borderRadius(24)
    .height(48);
}

@Component
struct StyledCard {
  build() {
    Column() {
      Text('卡片标题').titleStyle(20);
      Text('卡片内容').fontSize(14).fontColor('#666').margin({ top: 8 });
      Button('操作').primaryStyle().margin({ top: 16 });
    }
    .width('100%')
    .cardPadding()
    .cardShadow()
    .borderRadius(12)
    .backgroundColor(Color.White);
  }
}

5.8 @Reusable 组件复用:列表项

interface NewsItem {
  id: number;
  title: string;
  source: string;
  image: string;
}

// @Reusable:标记可复用组件
@Reusable
@Component
struct NewsRow {
  @Prop item: NewsItem;
  @State liked: boolean = false;
  onItemClick?: (id: number) => void;

  // 复用时回调:重置内部状态
  aboutToReuse(params: ESObject) {
    this.liked = false; // 重置点赞状态
    console.info(`[NewsRow] reuse, item.id=${this.item.id}`);
  }

  build() {
    Row() {
      Image(this.item.image)
        .width(80).height(80).borderRadius(8);

      Column() {
        Text(this.item.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(2);
        Row() {
          Text(this.item.source).fontSize(12).fontColor('#999');
          Text(this.liked ? '已赞' : '点赞')
            .fontSize(12)
            .fontColor(this.liked ? '#FF4444' : '#999')
            .margin({ left: 16 })
            .onClick(() => this.liked = !this.liked);
        }
        .margin({ top: 8 });
      }
      .layoutWeight(1)
      .margin({ left: 12 })
      .alignItems(HorizontalAlign.Start);
    }
    .width('100%')
    .padding(12)
    .onClick(() => this.onItemClick?.(this.item.id));
  }
}

@Entry
@Component
struct NewsList {
  private data: NewsItem[] = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    title: `新闻标题 ${i}`,
    source: `来源 ${i}`,
    image: `https://example.com/${i}.png`,
  }));

  build() {
    List({ space: 8 }) {
      LazyForEach(this.data, (item: NewsItem) => {
        ListItem() {
          // @Reusable 组件:滑动时复用,避免重建
          NewsRow({ item, onItemClick: (id) => {
            console.info(`点击新闻 ${id}`);
          } });
        }
      }, (item: NewsItem) => item.id.toString());
    }
    .cachedCount(5); // 预渲染 5 个
  }
}

5.9 @Watch 状态监听:埋点与持久化

import { preferences } from '@kit.ArkData';

@Component
struct SearchBar {
  @Prop keyword: string = '';
  // @Watch:监听 keyword 变化,触发埋点
  @State @Watch('onKeywordChange') inputValue: string = '';

  // 监听回调:记录用户输入行为
  onKeywordChange(attrName: string) {
    console.info(`[埋点] 搜索词变化:${this.inputValue}`);
    // 防抖:500ms 后请求
    this.debounceSearch();
  }

  private timerId: number = -1;
  // 防抖搜索
  debounceSearch() {
    if (this.timerId !== -1) clearTimeout(this.timerId);
    this.timerId = setTimeout(() => {
      this.persistHistory();
    }, 500);
  }

  // 持久化搜索历史
  async persistHistory() {
    const prefs = await preferences.getPreferences({ name: 'search_history' });
    await prefs.put(this.inputValue, Date.now());
    await prefs.flush();
  }

  build() {
    TextInput({ text: this.inputValue })
      .placeholder('搜索')
      .onChange((value) => {
        this.inputValue = value;
      });
  }
}

5.10 综合案例:购物车商品行

interface CartItem {
  id: number;
  name: string;
  price: number;
  image: string;
  quantity: number;
  selected: boolean;
}

@Observed
class CartItemModel implements CartItem {
  id: number;
  name: string;
  price: number;
  image: string;
  quantity: number;
  selected: boolean;

  constructor(data: CartItem) {
    this.id = data.id;
    this.name = data.name;
    this.price = data.price;
    this.image = data.image;
    this.quantity = data.quantity;
    this.selected = data.selected;
  }
}

@Reusable
@Component
struct CartRow {
  // @ObjectLink:与 @Observed 配合,追踪嵌套对象变化
  @ObjectLink item: CartItemModel;
  // @Link:选中状态双向绑定
  @Link isSelected: boolean;
  onQuantityChange?: (id: number, qty: number) => void;

  aboutToReuse(params: ESObject) {
    // 复用时重置内部状态(如有)
  }

  build() {
    Row() {
      // 选中框
      Checkbox()
        .select(this.isSelected)
        .onChange((v) => this.isSelected = v);

      Image(this.item.image)
        .width(80).height(80).borderRadius(8);

      Column() {
        Text(this.item.name).fontSize(16).maxLines(2);
        Text(`¥${this.item.price}`).fontSize(16).fontColor('#FF4444').margin({ top: 4 });
        Row() {
          Button('-')
            .width(32).height(32).fontSize(18)
            .onClick(() => {
              if (this.item.quantity > 1) {
                this.item.quantity--;
                this.onQuantityChange?.(this.item.id, this.item.quantity);
              }
            });
          Text(`${this.item.quantity}`)
            .fontSize(16)
            .margin({ left: 12, right: 12 });
          Button('+')
            .width(32).height(32).fontSize(18)
            .onClick(() => {
              this.item.quantity++;
              this.onQuantityChange?.(this.item.id, this.item.quantity);
            });
        }
        .margin({ top: 8 });
      }
      .layoutWeight(1)
      .margin({ left: 12 })
      .alignItems(HorizontalAlign.Start);
    }
    .width('100%')
    .padding(12);
  }
}

@Entry
@Component
struct CartPage {
  @State items: CartItemModel[] = [
    new CartItemModel({ id: 1, name: '商品A', price: 99, image: 'a.png', quantity: 1, selected: false }),
    new CartItemModel({ id: 2, name: '商品B', price: 199, image: 'b.png', quantity: 2, selected: true }),
  ];
  @State selectedMap: Map<number, boolean> = new Map();

  build() {
    Column() {
      Text('购物车').fontSize(24).fontWeight(FontWeight.Bold).padding(16);
      List({ space: 8 }) {
        ForEach(this.items, (item: CartItemModel) => {
          ListItem() {
            CartRow({
              item,
              isSelected: this.selectedMap.get(item.id) || false,
              onQuantityChange: (id, qty) => {
                console.info(`商品 ${id} 数量变为 ${qty}`);
              },
            });
          }
        }, (item: CartItemModel) => item.id.toString());
      }
      .layoutWeight(1);
    };
  }
}

6. 对比分析

6.1 ArkUI 自定义组件 vs React Function Component

维度ArkUIReact FC差异分析
声明形式struct + @Component + build()function + JSX returnArkUI 更接近 SwiftUI
状态管理@State/@Prop/@Link 装饰器useState/useReducer HookArkUI 编译期插桩,React 运行时 Hook
生命周期aboutToAppear/aboutToDisappearuseEffect/useLayoutEffectArkUI 显式回调,React 副作用函数
插槽@BuilderParamchildren prop等价,ArkUI 语法更声明式
复用@Reusable + 复用池React.memo + memoizationArkUI 实例级复用,React 渲染级 memo
性能编译期 AOT,无 Virtual DOM运行时 Virtual DOM DiffArkUI 性能更优
生态HarmonyOS 专属跨平台React 生态更丰富

6.2 ArkUI vs Vue SFC

维度ArkUIVue SFC差异分析
声明形式struct + ArkTS.vue 文件 + template/script/styleVue 三段式更熟悉
状态管理@Stateref/reactiveArkUI 装饰器,Vue Composition API
双向绑定@Link + $ 语法v-model语法相似,实现不同
插槽@BuilderParamVue 语法更简洁
复用@Reusablekeep-alive机制类似
编译ArkTS 编译期 AOTvue-loader 运行时ArkUI 性能更优

6.3 ArkUI vs SwiftUI

维度ArkUISwiftUI差异分析
声明形式struct + @Componentstruct View + body极为相似
状态管理@State/@Binding@State/@Binding装饰器命名借鉴 SwiftUI
插槽@BuilderParam@ViewBuilder + ViewBuilder机制相同
复用@ReusableSwiftUI 无显式复用ArkUI 多了复用池
语言ArkTS(TS 超集)SwiftArkTS 更易学

6.4 ArkUI vs Jetpack Compose

维度ArkUICompose差异分析
声明形式struct + @Component@Composable functionCompose 更函数式
状态管理@Stateremember + mutableStateOfArkUI 装饰器更简洁
插槽@BuilderParamchildren lambda等价
复用@Reusablekey + remember机制不同
编译ArkTS AOTKotlin Compiler Plugin都为编译期变换

6.5 @BuilderParam vs React children vs Vue slot

维度@BuilderParamReact childrenVue slot
声明@BuilderParam content: () => voidchildren: ReactNode
传递Card() { Text(‘hi’) }hi<template #content>hi
多插槽多个 @BuilderParam命名 props命名 slot
类型检查强类型(函数签名)弱类型(ReactNode)弱类型

6.6 @Reusable vs React.memo vs Vue keep-alive

维度@ReusableReact.memoVue keep-alive
复用粒度组件实例渲染结果组件实例
触发条件类型 + reuseId 匹配props 浅比较手动 include/exclude
状态保留是(aboutToReuse 重置)否(每次重新渲染)
性能收益避免实例创建销毁避免渲染避免实例创建销毁

7. 常见陷阱与反模式

7.1 陷阱:在 build() 中执行副作用

// 反模式:在 build() 中发起网络请求
@Component
struct BadComponent {
  @State data: string = '';

  build() {
    // 错误:build() 可能被多次调用,导致重复请求
    fetch('/api/data').then(res => this.data = res);
    Text(this.data);
  }
}

// 正确:在 aboutToAppear 中发起
@Component
struct GoodComponent {
  @State data: string = '';

  aboutToAppear() {
    fetch('/api/data').then(res => this.data = res);
  }

  build() {
    Text(this.data);
  }
}

事故案例:某电商应用首页在 build() 中调用 localStorage.get(),导致每次状态变化都触发磁盘 I/O,首页滑动卡顿。修复后 FPS 从 30 提升至 60。

7.2 陷阱:@Prop 传递大对象

// 反模式:@Prop 传递 1000 条数据
@Component
struct BadList {
  @Prop items: Array<Item>; // 每次 1000 条深拷贝

  build() {
    List() {
      ForEach(this.items, (item) => { Text(item.name); });
    }
  }
}

// 正确:使用 @Link 或 @ObjectLink
@Component
struct GoodList {
  @Link items: Array<Item>; // 引用,无拷贝

  build() {
    List() {
      ForEach(this.items, (item) => { Text(item.name); });
    }
  }
}

事故案例:某新闻应用列表使用 @Prop 传递 500 条新闻,每次刷新导致主线程阻塞 200ms。改用 @Link 后降至 5ms。

7.3 陷阱:滥用 @Provide/@Consume 导致全局污染

// 反模式:所有状态都用 @Provide
@Entry
@Component
struct App {
  @Provide('user') user: User;
  @Provide('cart') cart: Cart;
  @Provide('theme') theme: Theme;
  @Provide('locale') locale: string;
  // ... 过度使用
}

// 正确:仅跨多层组件的状态用 @Provide,局部状态用 @State/@Link
@Entry
@Component
struct App {
  @Provide('theme') theme: Theme; // 全局主题合理
}

@Component
struct CartPage {
  @State cart: Cart; // 局部状态,无需全局
}

事故案例:某应用将 30+ 状态全部 @Provide,导致任意组件变化都触发整树重建,首页加载时间从 500ms 升至 2s。

7.4 陷阱:@Reusable 未重置内部状态

// 反模式:@Reusable 组件未重置状态
@Reusable
@Component
struct BadRow {
  @Prop item: Item;
  @State liked: boolean = false; // 复用时保留旧状态!

  build() { /* ... */ }
}

// 正确:实现 aboutToReuse
@Reusable
@Component
struct GoodRow {
  @Prop item: Item;
  @State liked: boolean = false;

  aboutToReuse(params: ESObject) {
    this.liked = false; // 重置
  }

  build() { /* ... */ }
}

事故案例:某社交应用列表项的”点赞”状态在复用后保留,导致用户看到错误的点赞状态。修复后投诉量下降 80%。

7.5 陷阱:ForEach 的 key 使用 index

// 反模式:用 index 作为 key
ForEach(this.items, (item, index) => {
  Row() { Text(item.name); }
}, (item, index) => index.toString()); // 不稳定!

// 正确:用唯一 id
ForEach(this.items, (item) => {
  Row() { Text(item.name); }
}, (item) => item.id.toString());

事故案例:某待办列表使用 index 作为 key,删除第一项后所有项的状态错位(第二项变成第一项的状态)。改用 id 后正常。

7.6 陷阱:在 aboutToDisappear 中访问已销毁的资源

// 反模式:异步回调中访问已销毁组件
@Component
struct BadComponent {
  @State data: string = '';

  aboutToAppear() {
    setTimeout(() => {
      // 组件可能已销毁,this.data 无效
      this.data = 'updated';
    }, 5000);
  }

  build() { Text(this.data); }
}

// 正确:清理定时器
@Component
struct GoodComponent {
  @State data: string = '';
  private timerId: number = -1;

  aboutToAppear() {
    this.timerId = setTimeout(() => {
      this.data = 'updated';
    }, 5000);
  }

  aboutToDisappear() {
    if (this.timerId !== -1) clearTimeout(this.timerId);
  }

  build() { Text(this.data); }
}

7.7 陷阱:过度使用 if/else 切换

// 反模式:频繁 if/else 切换
@Component
struct BadPage {
  @State tab: number = 0;

  build() {
    if (this.tab === 0) { TabA(); }
    else if (this.tab === 1) { TabB(); }
    else { TabC(); }
  }
}

// 正确:用 visibility 隐藏
@Component
struct GoodPage {
  @State tab: number = 0;

  build() {
    Stack() {
      TabA().visibility(this.tab === 0 ? Visibility.Visible : Visibility.Hidden);
      TabB().visibility(this.tab === 1 ? Visibility.Visible : Visibility.Hidden);
      TabC().visibility(this.tab === 2 ? Visibility.Visible : Visibility.Hidden);
    }
  }
}

7.8 陷阱:@BuilderParam 与 @State 混淆作用域

// 反模式:在 @Builder 中错误引用 this
@Component
struct Parent {
  @State count: number = 0;

  @Builder
  content() {
    // 这里的 this 是 Parent,但传递给子组件后可能丢失
    Text(`${this.count}`);
  }

  build() {
    Child() {
      this.content(); // 看似正常,实则闭包陷阱
    }
  }
}

// 正确:通过参数传递
@Component
struct Parent {
  @State count: number = 0;

  @Builder
  content(count: number) {
    Text(`${count}`);
  }

  build() {
    Child() {
      this.content(this.count);
    }
  }
}
// 反模式:直接传值,未建立双向绑定
@Component
struct Parent {
  @State value: number = 0;

  build() {
    Child({ value: this.value }); // 单向,子组件修改不回传
  }
}

// 正确:使用 $ 语法
@Component
struct Parent {
  @State value: number = 0;

  build() {
    Child({ value: $value }); // 双向绑定
  }
}

7.10 陷阱:组件嵌套过深导致性能下降

// 反模式:10 层嵌套
@Component
struct DeepNested {
  build() {
    A() {
      B() {
        C() {
          D() {
            E() { Text('hi'); }
          }
        }
      }
    }
  }
}

// 正确:扁平化或提取子组件
@Component
struct Flat {
  build() {
    E() { Text('hi'); }
  }
}

事故案例:某应用商品详情页嵌套 12 层,首屏渲染 800ms。重构为 3 层后降至 200ms。


8. 工程实践

8.1 组件库设计原则

设计 ArkUI 组件库时应遵循:

  1. 单一职责:每个组件只做一件事,如 Card 只负责容器,CardHeader 负责头部。
  2. 开放扩展:通过 @BuilderParam 插槽支持自定义内容,而非硬编码。
  3. 封闭修改:组件内部状态不对外暴露,通过事件回调通信。
  4. 类型安全:所有 Props 强类型,避免 any
  5. 主题适配:通过 @Provide/@Consume 注入主题,而非硬编码颜色。
  6. 无障碍:支持 accessibilityTextaccessibilityRole 等属性。

8.2 组件性能监控

// 组件性能监控装饰器(伪代码,需 ArkTS 编译器插件支持)
function withPerfMonitor(target: ESObject, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: ESObject[]) {
    const start = Date.now();
    const result = original.apply(this, args);
    const duration = Date.now() - start;
    if (duration > 16) { // 超过一帧
      console.warn(`[Perf] ${target.constructor.name}.${key} took ${duration}ms`);
    }
    return result;
  };
}

@Component
struct MonitoredComponent {
  @withPerfMonitor
  build() {
    // 监控 build 耗时
  }
}

8.3 组件懒加载

// 使用 LazyForEach 懒加载列表
@Entry
@Component
struct LazyList {
  private data: IDataSource = new LazyDataSource();

  build() {
    List() {
      LazyForEach(this.data, (item: Item) => {
        ListItem() {
          ItemRow({ item });
        }
      }, (item: Item) => item.id.toString());
    }
    .cachedCount(5); // 预渲染 5 个
  }
}

class LazyDataSource implements IDataSource {
  private items: Item[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number { return this.items.length; }
  getData(idx: number): Item { return this.items[idx]; }
  registerDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }
  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listeners = this.listeners.filter(l => l !== listener);
  }
  // 按需加载数据
  async loadData(start: number, end: number) {
    const newItems = await fetchItems(start, end);
    this.items.push(...newItems);
    this.listeners.forEach(l => l.onDataChange(`add`, start, end));
  }
}

8.4 组件测试

// 组件单元测试(需配合 ArkUI 测试框架)
import { describe, it, expect } from '@ohos/hypium';

export default function abilityTest() {
  describe('CardComponent', () => {
    it('should render title', 0, () => {
      const driver = Driver.create();
      const comp = driver.createComponent(Card, { title: 'Test' });
      const text = comp.findComponent(Text);
      expect(text.getText()).assertEqual('Test');
    });

    it('should trigger onAction click', 0, () => {
      let clicked = false;
      const driver = Driver.create();
      const comp = driver.createComponent(Card, {
        title: 'Test',
        showAction: true,
        onAction: () => { clicked = true; },
      });
      comp.findComponent(Text, { text: '更多' }).click();
      expect(clicked).assertTrue();
    });
  });
}

8.5 组件文档生成

// 使用 JSDoc 注解生成组件文档
/**
 * 通用卡片组件
 * @example
 * ```typescript
 * Card({ title: '标题' }) {
 *   Text('内容');
 * }
 * ```
 */
@Component
export struct Card {
  /**
   * 卡片标题
   * @type {string}
   * @default ''
   */
  @Prop title: string = '';

  /**
   * 是否显示右上角操作按钮
   * @type {boolean}
   * @default false
   */
  @Prop showAction: boolean = false;

  /**
   * 点击操作按钮的回调
   */
  onAction?: () => void;

  /**
   * 自定义内容插槽
   */
  @BuilderParam content: () => void;

  build() { /* ... */ }
}

8.6 组件版本管理

// 版本化组件:支持渐进式迁移
@Component
export struct CardV1 {
  @Prop title: string;
  @BuilderParam content: () => void;
  build() { /* V1 实现 */ }
}

@Component
export struct CardV2 {
  @Prop title: string;
  @Prop subtitle?: string; // 新增
  @Prop variant?: 'default' | 'outlined'; // 新增
  @BuilderParam content: () => void;
  @BuilderParam footer?: () => void; // 新增插槽
  build() { /* V2 实现 */ }
}

// 别名:默认导出最新版本
export { CardV2 as Card };

8.7 组件主题化

// 主题令牌
interface ThemeTokens {
  colors: {
    primary: string;
    background: string;
    text: string;
  };
  spacing: {
    small: number;
    medium: number;
    large: number;
  };
  borderRadius: {
    small: number;
    medium: number;
  };
}

const lightTokens: ThemeTokens = {
  colors: { primary: '#007DFF', background: '#FFFFFF', text: '#1a1a1a' },
  spacing: { small: 8, medium: 16, large: 24 },
  borderRadius: { small: 4, medium: 12 },
};

// 根组件提供主题
@Entry
@Component
struct App {
  @Provide('tokens') tokens: ThemeTokens = lightTokens;
  build() { /* ... */ }
}

// 子组件消费主题
@Component
struct ThemedButton {
  @Consume('tokens') tokens: ThemeTokens;
  build() {
    Text('按钮')
      .padding(this.tokens.spacing.medium)
      .backgroundColor(this.tokens.colors.primary)
      .borderRadius(this.tokens.borderRadius.medium);
  }
}

9. 案例研究

9.1 案例一:电商商品列表性能优化

场景:某电商应用商品列表 1000 项,滑动卡顿,FPS 20。

诊断

  1. 列表项使用 @Prop 传递完整商品对象,每次更新深拷贝 1000 次。
  2. 未使用 @Reusable,滑动时频繁创建销毁组件。
  3. 图片未懒加载,所有 1000 张图片同时请求。

优化方案

  1. @Prop 改为 @Link,避免深拷贝。
  2. 添加 @Reusable 装饰器,实现 aboutToReuse
  3. 使用 LazyForEach + cachedCount(5)
  4. 图片组件添加 syncLoad(false),异步加载。

结果:FPS 从 20 提升至 58,内存占用降低 40%。

9.2 案例二:跨组件状态同步

场景:某社交应用需要全局用户状态、主题、语言切换,原方案通过 props 逐层传递,代码冗长且易错。

诊断

  1. 用户状态传递 5 层,每层都需声明 @Prop user
  2. 主题切换需手动通知所有组件。
  3. 语言切换后需重新渲染所有文本。

方案

  1. 使用 @Provide/@Consume 注入全局状态(user、theme、locale)。
  2. 使用 AppStorage 持久化用户偏好。
  3. 使用 @Watch 监听 locale 变化,触发文本重渲染。

结果:代码量减少 60%,状态同步延迟从 100ms 降至 10ms。

9.3 案例三:组件库设计与发布

场景:某企业需要开发内部组件库,供 10+ 应用复用。

设计

  1. 组件分层:基础组件(Button、Input)、容器组件(Card、List)、业务组件(UserCard、OrderItem)。
  2. 主题系统:通过 @Provide/@Consume 注入 ThemeTokens。
  3. 国际化:通过 @Provide 注入 i18n 实例。
  4. 无障碍:所有组件支持 accessibilityRoleaccessibilityText
  5. 按需加载:每个组件独立打包,支持 tree-shaking。

发布

  1. 编译为 .har 包,发布到内部 Maven 仓库。
  2. 提供完整 TypeScript 类型声明。
  3. 自动生成组件文档与示例。

结果:10 个应用接入,UI 一致性提升,开发效率提高 50%。

9.4 案例四:复杂表单组件

场景:某表单应用需要动态表单,字段数量与类型运行时决定。

设计

  1. 使用 @BuilderParam 渲染动态字段。
  2. 使用 @State 维护表单数据。
  3. 使用 @Watch 触发校验。
  4. 使用 @Provide 注入表单上下文。
interface FormField {
  name: string;
  label: string;
  type: 'text' | 'number' | 'select';
  required: boolean;
}

@Component
struct DynamicForm {
  @Prop fields: FormField[];
  @State values: Record<string, string> = {};
  @State errors: Record<string, string> = {};
  onSubmit?: (values: Record<string, string>) => void;

  @Watch('validate')
  onValueChange() {}

  validate() {
    this.fields.forEach(f => {
      if (f.required && !this.values[f.name]) {
        this.errors[f.name] = `${f.label}不能为空`;
      }
    });
  }

  build() {
    Column({ space: 12 }) {
      ForEach(this.fields, (field: FormField) => {
        Column() {
          Text(field.label).fontSize(14);
          TextInput({ text: this.values[field.name] || '' })
            .onChange((v) => {
              this.values[field.name] = v;
            });
          if (this.errors[field.name]) {
            Text(this.errors[field.name])
              .fontSize(12)
              .fontColor('#FF4444');
          }
        }
        .alignItems(HorizontalAlign.Start);
      });

      Button('提交')
        .onClick(() => {
          this.validate();
          if (Object.keys(this.errors).length === 0) {
            this.onSubmit?.(this.values);
          }
        });
    }
  }
}

10. 习题

10.1 基础题

Q1:ArkUI 自定义组件有哪六个生命周期回调?分别说明触发时机。

Q2@Prop@Link 的本质区别是什么?为何 @Prop 会导致深拷贝?

Q3@BuilderParam 与 React 的 children prop 有何异同?

Q4@Styles@Extend 的区别是什么?分别适用于什么场景?

Q5@Reusable 复用池的命中条件是什么?未命中时如何退化?

10.2 进阶题

Q6:分析以下代码的性能问题并给出优化方案:

@Component
struct ProductList {
  @Prop products: Product[]; // 可能 1000 条

  build() {
    List() {
      ForEach(this.products, (product, index) => {
        ProductRow({ product });
      }, (product, index) => index.toString());
    }
  }
}

Q7:设计一个支持主题切换的按钮组件,要求:

  1. 通过 @Consume 获取主题。
  2. 支持 primarysecondarydanger 三种样式。
  3. 支持点击事件。
  4. 支持禁用状态。

Q8:解释为何在 build() 中执行副作用(如网络请求)是反模式,给出 ArkUI 推荐的替代方案。

Q9:分析 @Provide/@ConsumeAppStorage 的区别,分别说明适用场景。

Q10:实现一个 @Reusable 的列表项组件,要求:

  1. 显示用户头像、姓名、签名。
  2. 支持”关注”按钮,关注状态在复用时正确重置。
  3. 点击头像触发回调。

10.3 挑战题

Q11:设计一个企业级组件库的基础架构,包含:

  1. 主题系统(设计令牌、主题切换)。
  2. 国际化(多语言、RTL 支持)。
  3. 无障碍(ARIA 属性、键盘导航)。
  4. 按需加载(tree-shaking)。
  5. 版本管理(向后兼容)。

Q12:某长列表应用在低端设备上滑动卡顿,FPS 仅 15。请给出完整的性能优化方案,包含:

  1. 诊断方法(如何定位瓶颈)。
  2. 优化策略(@ReusableLazyForEach、图片懒加载、避免深拷贝)。
  3. 验证方法(如何测量优化效果)。

Q13:分析 ArkUI 选择”struct + 装饰器”而非”class 继承”或”函数式组件”的工程动机,从以下维度论证:

  1. 编译期优化(AOT、AST 变换)。
  2. 开发者体验(TypeScript 类型、IDE 支持)。
  3. 性能(避免 Virtual DOM Diff)。
  4. 生态兼容(与现有 TS 库互操作)。

11. 参考文献

  1. Huawei. ArkUI Developer Documentation. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ark-ui-0000001501453337

  2. Huawei. ArkTS Language Specification. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-specification-0000001774279709

  3. Huawei. @Reusable Decorator: Component Reuse. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-reusable-0000001774279618

  4. Huawei. @BuilderParam Decorator. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builderparam-0000001774279606

  5. Apple Inc. SwiftUI Declarations. Apple Developer Documentation. 2024. https://developer.apple.com/documentation/swiftui

  6. Google. Jetpack Compose. Android Developer Documentation. 2024. https://developer.android.com/jetpack/compose

  7. Meta. React Hooks. React Documentation. 2024. https://react.dev/reference/react

  8. Evan You. Vue 3 Composition API. Vue.js Documentation. 2024. https://vuejs.org/guide/extras/composition-api-faq.html

  9. Sebastian Markbåge. React Fiber Architecture. 2017. https://github.com/acdlite/react-fiber-architecture

  10. Lee Byron. React Component Performance. React Conf. 2016. https://www.youtube.com/watch?v=KYzlpRv-WZs

  11. Anderson, C. R. et al. 2024. Optimizing Declarative UI Performance with Compile-Time Analysis. Proceedings of the ACM on Programming Languages 8, OOPSLA. https://doi.org/10.1145/3649820

  12. Zhang, Y. et al. 2023. ArkUI: A High-Performance Declarative UI Framework for HarmonyOS. In Proceedings of the 38th IEEE/ACM International Conference on Automated Software Engineering (ASE ‘23). IEEE. https://doi.org/10.1109/ASE56229.2023.00145

  13. Li, M. and Chen, X. 2024. Compile-Time State Tracking in Declarative UI Frameworks. IEEE Transactions on Software Engineering 50(4): 1234-1250. https://doi.org/10.1109/TSE.2024.3421198


12. 延伸阅读


附录 A:ArkUI 自定义组件装饰器速查表

装饰器用途适用场景
@Component标记自定义组件所有自定义 struct
@Entry标记页面根组件页面入口组件
@State组件内部状态需触发重建的本地变量
@Prop单向数据流父传子,子只读
@Link双向绑定父子同步
@Provide跨层级注入祖先向后代提供
@Consume跨层级消费后代消费祖先注入
@ObjectLink嵌套对象响应式与 @Observed 配合
@Observed标记可观察类类装饰器
@Watch监听状态变化触发副作用
@Builder声明 UI 片段复用 UI 结构
@BuilderParam声明插槽组件内容插槽
@Styles复用样式属性通用样式集合
@Extend扩展原生组件方法带参数的样式
@Reusable组件复用长列表性能优化
@StorageLink全局存储双向绑定AppStorage 同步
@StorageProp全局存储单向AppStorage 只读
@LocalStorageLink局部存储双向LocalStorage 同步
@LocalStorageProp局部存储单向LocalStorage 只读

附录 B:生命周期回调速查表

回调触发时机典型用途
aboutToAppear组件创建后,build() 前数据初始化、网络请求
aboutToDisappear组件销毁前清理定时器、取消请求
onPageShow页面切入前台刷新数据、恢复动画
onPageHide页面切到后台暂停动画、保存状态
onBackPress用户按返回键拦截返回、确认弹窗
aboutToReuse@Reusable 组件复用时重置内部状态

附录 C:性能优化速查表

优化点反模式正模式收益
大对象传递@Prop 深拷贝@Link 引用避免拷贝
列表渲染ForEach + index keyForEach + id key + @Reusable避免重建
条件渲染频繁 if/else 切换visibility 隐藏保留 Element
副作用build() 中执行aboutToAppear 中执行避免重复触发
异步回调未清理定时器aboutToDisappear 清理避免内存泄漏
图片加载同步加载syncLoad(false)避免阻塞主线程
组件嵌套10+ 层嵌套扁平化或提取子组件减少构建成本
返回入门指南