自定义组件
自定义组件与生命周期
自定义组件: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 自定义组件的六个生命周期回调:
aboutToAppear、aboutToDisappear、onPageShow、onPageHide、onBackPress、aboutToReuse(@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的”插槽”机制与 Reactchildren/Vue<slot>/Web Components<slot>的等价性。 - U4:对比
@Styles与@Extend:@Styles是属性集合的复用,@Extend是方法扩展,两者本质是编译期宏展开。 - U5:解释
@Reusable复用池的”LRU + 类型匹配”策略:仅当新节点与池中节点类型、@Reusableid 一致时才命中。 - U6:阐明
aboutToAppear与build()的执行顺序:前者在组件创建后、首次build()前执行,用于异步数据初始化。
1.3 Apply(应用)
- A1:使用
@Component+@Prop+@BuilderParam实现一个带插槽的卡片组件,支持自定义头部与内容。 - A2:使用
@Reusable+aboutToReuse实现一个列表项复用组件,避免滑动时的重建开销。 - A3:使用
@Styles提取通用按钮样式,使用@Extend为Text扩展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复用机制相比 Reactmemo、Vuekeep-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 到编译期生成代码的转变:
| 年代 | 框架 | 模型 | 关键特征 |
|---|---|---|---|
| 2014 | React 0.13 | Function Component + JSX | 运行时 Virtual DOM Diff |
| 2016 | Vue 2.0 | SFC + Options API | 响应式 + 模板编译 |
| 2018 | SwiftUI 1.0 | struct View + ViewBuilder | 编译期生成渲染树 |
| 2019 | Jetpack Compose 1.0 | @Composable + Kotlin Compiler | 编译期插桩 + Positional Memoization |
| 2020 | Flutter 1.0 | Widget + Element + RenderObject | 三树模型 |
| 2021 | ArkUI 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 树。
- 生命周期仅
aboutToAppear、aboutToDisappear、onPageShow、onPageHide、onBackPress。 - 无组件复用机制,列表滑动时频繁创建销毁。
2.3 ArkUI 2.0(2022):状态管理增强
HarmonyOS 3.1 引入 ArkUI 2.0:
- 引入
@Watch装饰器,支持监听状态变化。 - 引入
@ObjectLink+@Observed,支持嵌套对象的响应式。 - 引入
AppStorage与LocalStorage,实现全局与局部状态存储。 - 引入
@StorageLink、@StorageProp、@LocalStorageLink、@LocalStorageProp。 - 组件间通信能力完善,但仍无复用机制。
2.4 ArkUI 3.0(2023):复用与性能
HarmonyOS 4.0 引入 ArkUI 3.0:
- 引入
@Reusable装饰器与aboutToReuse生命周期,实现组件复用池。 - 引入
LazyForEach懒加载,配合@Reusable优化长列表性能。 - 引入
@AnimatableExtend,支持自定义动画属性。 - 编译器优化:build() 方法编译期生成 RenderFunction,减少运行时开销。
- 引入
if/else与ForEach的条件渲染追踪,避免无效 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.0 | 1.0 | 基础装饰器矩阵 |
| 3.2 | 2.0 | @Watch、@ObjectLink、AppStorage |
| 4.0 | 3.0 | @Reusable、LazyForEach 优化 |
| 5.0 | 4.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 自定义组件为七元组:
其中:
- 为组件内部状态集合,由
@State、@Provide等装饰器声明。 - 为组件输入属性集合,由
@Prop、@Link、@BuilderParam等声明。 - 为生命周期回调集合,包含
aboutToAppear、aboutToDisappear等。 - 为构建函数,输入构建上下文,输出 UI 树。
- 为复用池(仅
@Reusable组件),存储可复用的组件实例。 - 为组件发出的事件流,通过回调函数向父组件传递。
- 为组件元数据,包含组件名、参数类型、装饰器信息等,由编译期生成。
3.2 组件实例的生命周期
组件实例 的生命周期状态转换:
状态转移:
3.3 单向数据流的形式化
@Prop 单向数据流的语义:
子组件对 @Prop 的修改仅影响本地拷贝,不回传父组件。当父组件 变化时,触发子组件 的重新构建:
3.4 双向绑定的形式化
@Link 双向绑定的语义:
任一方的修改都会同步到另一方。实现上,@Link 建立的是对父组件状态对象的”引用”(通过 ES Proxy 或 ArkUI 内部状态对象),而非值拷贝。
3.5 跨层级传递的形式化
@Provide/@Consume 的语义:
祖先组件 通过 @Provide 声明一个”注入值”,后代组件 通过 @Consume “消费”该值。注入与消费通过 key 匹配,key 可为字符串或 symbol。
3.6 复用池的形式化
@Reusable 复用池的工作机制:
当新的同类型组件 需要创建时:
复用池采用 LRU(Least Recently Used)策略,容量上限默认为 128 个实例。
3.7 构建函数的纯函数性
理想情况下,build() 应为纯函数:
即给定相同的状态与属性,应输出相同的 UI 树。但实际上,build() 中可包含副作用(如日志、埋点),ArkUI 不强制纯函数性,但建议遵循单向数据流原则。
3.8 依赖追踪的复杂度
ArkUI 的响应式系统对 @State 的依赖追踪复杂度:
- 首次构建:, 为 build() 中读取的
@State数量。 - 后续更新:, 为变化的
@State数量,仅触发依赖该状态的子树重建。
4. 理论推导与原理解析
4.1 声明式 UI 的核心不变量
声明式 UI 的核心不变量是”UI 是状态的函数”:
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)成本为 ,但对于对象数组成本为 :
对于大对象(如 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 复用池的命中条件:
其中 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 机制
ForEach 的 keyGenerator 决定列表项的复用:
ForEach(arr, (item) => { Item({ item }) }, (item) => item.id)
- key 稳定:列表项重排时,Element 复用,仅调整位置,性能 。
- key 不稳定(如用 index):列表项重排时,Element 销毁重建,性能 。
形式化:
4.8 条件渲染的依赖追踪
if/else 分支在 ArkUI 中会被追踪:
if (this.isLoading) {
Loading();
} else {
Content();
}
- 首次构建:根据
isLoading选择分支,注册依赖。 isLoading变化:销毁旧分支 Element,创建新分支 Element。
这意味着 if/else 切换是有成本的,频繁切换会导致 Element 频繁创建销毁。替代方案:使用 visibility 属性隐藏,保留 Element。
4.9 组件复用与内存占用
@Reusable 复用池的内存占用:
复用池默认容量 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);
}
}
5.4 @Link 双向绑定:开关组件
@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
| 维度 | ArkUI | React FC | 差异分析 |
|---|---|---|---|
| 声明形式 | struct + @Component + build() | function + JSX return | ArkUI 更接近 SwiftUI |
| 状态管理 | @State/@Prop/@Link 装饰器 | useState/useReducer Hook | ArkUI 编译期插桩,React 运行时 Hook |
| 生命周期 | aboutToAppear/aboutToDisappear | useEffect/useLayoutEffect | ArkUI 显式回调,React 副作用函数 |
| 插槽 | @BuilderParam | children prop | 等价,ArkUI 语法更声明式 |
| 复用 | @Reusable + 复用池 | React.memo + memoization | ArkUI 实例级复用,React 渲染级 memo |
| 性能 | 编译期 AOT,无 Virtual DOM | 运行时 Virtual DOM Diff | ArkUI 性能更优 |
| 生态 | HarmonyOS 专属 | 跨平台 | React 生态更丰富 |
6.2 ArkUI vs Vue SFC
| 维度 | ArkUI | Vue SFC | 差异分析 |
|---|---|---|---|
| 声明形式 | struct + ArkTS | .vue 文件 + template/script/style | Vue 三段式更熟悉 |
| 状态管理 | @State | ref/reactive | ArkUI 装饰器,Vue Composition API |
| 双向绑定 | @Link + $ 语法 | v-model | 语法相似,实现不同 |
| 插槽 | @BuilderParam | Vue 语法更简洁 | |
| 复用 | @Reusable | keep-alive | 机制类似 |
| 编译 | ArkTS 编译期 AOT | vue-loader 运行时 | ArkUI 性能更优 |
6.3 ArkUI vs SwiftUI
| 维度 | ArkUI | SwiftUI | 差异分析 |
|---|---|---|---|
| 声明形式 | struct + @Component | struct View + body | 极为相似 |
| 状态管理 | @State/@Binding | @State/@Binding | 装饰器命名借鉴 SwiftUI |
| 插槽 | @BuilderParam | @ViewBuilder + ViewBuilder | 机制相同 |
| 复用 | @Reusable | SwiftUI 无显式复用 | ArkUI 多了复用池 |
| 语言 | ArkTS(TS 超集) | Swift | ArkTS 更易学 |
6.4 ArkUI vs Jetpack Compose
| 维度 | ArkUI | Compose | 差异分析 |
|---|---|---|---|
| 声明形式 | struct + @Component | @Composable function | Compose 更函数式 |
| 状态管理 | @State | remember + mutableStateOf | ArkUI 装饰器更简洁 |
| 插槽 | @BuilderParam | children lambda | 等价 |
| 复用 | @Reusable | key + remember | 机制不同 |
| 编译 | ArkTS AOT | Kotlin Compiler Plugin | 都为编译期变换 |
6.5 @BuilderParam vs React children vs Vue slot
| 维度 | @BuilderParam | React children | Vue slot |
|---|---|---|---|
| 声明 | @BuilderParam content: () => void | children: ReactNode | |
| 传递 | Card() { Text(‘hi’) } | <template #content>hi | |
| 多插槽 | 多个 @BuilderParam | 命名 props | 命名 slot |
| 类型检查 | 强类型(函数签名) | 弱类型(ReactNode) | 弱类型 |
6.6 @Reusable vs React.memo vs Vue keep-alive
| 维度 | @Reusable | React.memo | Vue 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);
}
}
}
7.9 陷阱:@Link 未使用 $ 语法
// 反模式:直接传值,未建立双向绑定
@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 组件库时应遵循:
- 单一职责:每个组件只做一件事,如
Card只负责容器,CardHeader负责头部。 - 开放扩展:通过
@BuilderParam插槽支持自定义内容,而非硬编码。 - 封闭修改:组件内部状态不对外暴露,通过事件回调通信。
- 类型安全:所有 Props 强类型,避免
any。 - 主题适配:通过
@Provide/@Consume注入主题,而非硬编码颜色。 - 无障碍:支持
accessibilityText、accessibilityRole等属性。
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。
诊断:
- 列表项使用
@Prop传递完整商品对象,每次更新深拷贝 1000 次。 - 未使用
@Reusable,滑动时频繁创建销毁组件。 - 图片未懒加载,所有 1000 张图片同时请求。
优化方案:
@Prop改为@Link,避免深拷贝。- 添加
@Reusable装饰器,实现aboutToReuse。 - 使用
LazyForEach+cachedCount(5)。 - 图片组件添加
syncLoad(false),异步加载。
结果:FPS 从 20 提升至 58,内存占用降低 40%。
9.2 案例二:跨组件状态同步
场景:某社交应用需要全局用户状态、主题、语言切换,原方案通过 props 逐层传递,代码冗长且易错。
诊断:
- 用户状态传递 5 层,每层都需声明
@Prop user。 - 主题切换需手动通知所有组件。
- 语言切换后需重新渲染所有文本。
方案:
- 使用
@Provide/@Consume注入全局状态(user、theme、locale)。 - 使用
AppStorage持久化用户偏好。 - 使用
@Watch监听 locale 变化,触发文本重渲染。
结果:代码量减少 60%,状态同步延迟从 100ms 降至 10ms。
9.3 案例三:组件库设计与发布
场景:某企业需要开发内部组件库,供 10+ 应用复用。
设计:
- 组件分层:基础组件(Button、Input)、容器组件(Card、List)、业务组件(UserCard、OrderItem)。
- 主题系统:通过
@Provide/@Consume注入 ThemeTokens。 - 国际化:通过
@Provide注入 i18n 实例。 - 无障碍:所有组件支持
accessibilityRole、accessibilityText。 - 按需加载:每个组件独立打包,支持 tree-shaking。
发布:
- 编译为 .har 包,发布到内部 Maven 仓库。
- 提供完整 TypeScript 类型声明。
- 自动生成组件文档与示例。
结果:10 个应用接入,UI 一致性提升,开发效率提高 50%。
9.4 案例四:复杂表单组件
场景:某表单应用需要动态表单,字段数量与类型运行时决定。
设计:
- 使用
@BuilderParam渲染动态字段。 - 使用
@State维护表单数据。 - 使用
@Watch触发校验。 - 使用
@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:设计一个支持主题切换的按钮组件,要求:
- 通过
@Consume获取主题。 - 支持
primary、secondary、danger三种样式。 - 支持点击事件。
- 支持禁用状态。
Q8:解释为何在 build() 中执行副作用(如网络请求)是反模式,给出 ArkUI 推荐的替代方案。
Q9:分析 @Provide/@Consume 与 AppStorage 的区别,分别说明适用场景。
Q10:实现一个 @Reusable 的列表项组件,要求:
- 显示用户头像、姓名、签名。
- 支持”关注”按钮,关注状态在复用时正确重置。
- 点击头像触发回调。
10.3 挑战题
Q11:设计一个企业级组件库的基础架构,包含:
- 主题系统(设计令牌、主题切换)。
- 国际化(多语言、RTL 支持)。
- 无障碍(ARIA 属性、键盘导航)。
- 按需加载(tree-shaking)。
- 版本管理(向后兼容)。
Q12:某长列表应用在低端设备上滑动卡顿,FPS 仅 15。请给出完整的性能优化方案,包含:
- 诊断方法(如何定位瓶颈)。
- 优化策略(
@Reusable、LazyForEach、图片懒加载、避免深拷贝)。 - 验证方法(如何测量优化效果)。
Q13:分析 ArkUI 选择”struct + 装饰器”而非”class 继承”或”函数式组件”的工程动机,从以下维度论证:
- 编译期优化(AOT、AST 变换)。
- 开发者体验(TypeScript 类型、IDE 支持)。
- 性能(避免 Virtual DOM Diff)。
- 生态兼容(与现有 TS 库互操作)。
11. 参考文献
-
Huawei. ArkUI Developer Documentation. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ark-ui-0000001501453337
-
Huawei. ArkTS Language Specification. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-specification-0000001774279709
-
Huawei. @Reusable Decorator: Component Reuse. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-reusable-0000001774279618
-
Huawei. @BuilderParam Decorator. HarmonyOS 5.0. 2024. https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builderparam-0000001774279606
-
Apple Inc. SwiftUI Declarations. Apple Developer Documentation. 2024. https://developer.apple.com/documentation/swiftui
-
Google. Jetpack Compose. Android Developer Documentation. 2024. https://developer.android.com/jetpack/compose
-
Meta. React Hooks. React Documentation. 2024. https://react.dev/reference/react
-
Evan You. Vue 3 Composition API. Vue.js Documentation. 2024. https://vuejs.org/guide/extras/composition-api-faq.html
-
Sebastian Markbåge. React Fiber Architecture. 2017. https://github.com/acdlite/react-fiber-architecture
-
Lee Byron. React Component Performance. React Conf. 2016. https://www.youtube.com/watch?v=KYzlpRv-WZs
-
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
-
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
-
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. 延伸阅读
- HarmonyOS 官方文档:ArkUI 组件
- OpenHarmony 源码:ArkUI 框架源码
- React Fiber:架构原理解析
- SwiftUI:Apple 官方教程
- Jetpack Compose:Android 官方教程
- 声明式 UI 理论:Out of the Tar Pit
- 响应式编程:Reactive Manifesto
- 组件设计原则:Material Design Components
附录 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 key | ForEach + id key + @Reusable | 避免重建 |
| 条件渲染 | 频繁 if/else 切换 | visibility 隐藏 | 保留 Element |
| 副作用 | build() 中执行 | aboutToAppear 中执行 | 避免重复触发 |
| 异步回调 | 未清理定时器 | aboutToDisappear 清理 | 避免内存泄漏 |
| 图片加载 | 同步加载 | syncLoad(false) | 避免阻塞主线程 |
| 组件嵌套 | 10+ 层嵌套 | 扁平化或提取子组件 | 减少构建成本 |