文章目录
-
- [一、API 接口详解](#一、API 接口详解)
-
- [1.1 onAppear](#1.1 onAppear)
- [1.2 onDisAppear](#1.2 onDisAppear)
- [1.3 onAttach](#1.3 onAttach)
- [1.4 onDetach](#1.4 onDetach)
- 二、基础用法示例
-
- [2.1 Toast 提示 + 日志记录](#2.1 Toast 提示 + 日志记录)
- [2.2 onAttach 与 onAppear 的时序验证](#2.2 onAttach 与 onAppear 的时序验证)
- 三、可运行完整示例:四种事件综合演示
- 四、实战场景:典型业务用法
-
- [4.1 onAppear 驱动数据懒加载](#4.1 onAppear 驱动数据懒加载)
- [4.2 onDisAppear 暂停/恢复动画](#4.2 onDisAppear 暂停/恢复动画)
- [4.3 onAttach / onDetach 管理事件监听](#4.3 onAttach / onDetach 管理事件监听)
- 五、与组件自定义生命周期的关系
- 总结
一、API 接口详解
1.1 onAppear
typescript
// 接口签名(API 9+)
onAppear(event: () => void): T
| 参数 | 类型 | 说明 |
|---|---|---|
event |
() => void |
组件进入可见状态时的回调,无参数 |
| 返回值 | 当前组件实例 T |
支持链式调用 |
触发场景:
- 组件首次渲染完成,进入屏幕可见区域
- 页面从后台切换到前台(App 切回)
- 路由从其他页面返回到当前页面
1.2 onDisAppear
typescript
// 接口签名(API 9+)
onDisAppear(event: () => void): T
触发场景:
- 条件渲染
if (isShow)中isShow变为false,组件被移除 - 页面切换到后台(App 切走)
- 路由跳转到其他页面
1.3 onAttach
typescript
// 接口签名(API 12+)
onAttach(callback: Callback<void>): T
| 特殊限制 | 说明 |
|---|---|
| 禁止修改组件树 | 此时布局渲染尚未完成,不能执行 if-else、启动动画、修改 @State 触发重渲 |
| 仅触发一次 | 每个组件实例生命周期内只调用一次,不随页面切换重复 |
| 适合做什么 | 注册事件监听、初始化非 UI 数据、记录组件创建时间戳 |
1.4 onDetach
typescript
// 接口签名(API 12+)
onDetach(callback: Callback<void>): T
| 特殊限制 | 说明 |
|---|---|
| 禁止修改组件树 | 与 onAttach 相同,渲染前触发,不能修改组件树 |
| 仅触发一次 | 每个组件实例生命周期内只调用一次 |
| 适合做什么 | 注销事件监听、释放定时器/网络请求、上报组件使用时长 |
二、基础用法示例
2.1 Toast 提示 + 日志记录
最直观的用法是在组件显隐时弹出 Toast 并记录日志,便于观察四个事件的实际触发顺序:
typescript
import { promptAction } from '@kit.ArkUI'
@Entry
@Component
struct BasicAppearDemo {
@State isShow: boolean = true
build() {
Column({ space: 20 }) {
Button(this.isShow ? '隐藏组件' : '显示组件')
.onClick(() => { this.isShow = !this.isShow })
if (this.isShow) {
Text('被监听的组件')
.fontSize(18).padding(20).backgroundColor('#E8EAF6').borderRadius(12)
// 渲染完成可见后触发
.onAppear(() => {
this.getUIContext().getPromptAction().showToast({
message: 'onAppear:组件已显示',
duration: 1500
})
console.info('[onAppear] 组件进入可见状态')
})
// 即将不可见时触发
.onDisAppear(() => {
this.getUIContext().getPromptAction().showToast({
message: 'onDisAppear:组件已隐藏',
duration: 1500
})
console.info('[onDisAppear] 组件即将不可见')
})
// 挂载到组件树时触发(渲染前,禁止修改树)
.onAttach(() => {
console.info('[onAttach] 已插入组件树(渲染前)')
})
// 从组件树卸载时触发(渲染前,禁止修改树)
.onDetach(() => {
console.info('[onDetach] 即将从组件树移除(渲染前)')
})
}
}
.width('100%').height('100%').alignItems(HorizontalAlign.Center)
.padding({ top: 60 })
}
}
控制台输出顺序(isShow: false → true):
[onAttach] 已插入组件树(渲染前)
[onAppear] 组件进入可见状态
(isShow: true → false)
[onDisAppear] 组件即将不可见
[onDetach] 即将从组件树移除(渲染前)
2.2 onAttach 与 onAppear 的时序验证
typescript
@Component
struct TimingDemo {
private createTime: number = 0
build() {
Column() {
Text('时序验证组件').fontSize(14).padding(16).backgroundColor('#F3E5F5').borderRadius(10)
}
// onAttach 先于 onAppear 触发
.onAttach(() => {
this.createTime = Date.now()
console.info(`[onAttach] 时间戳: ${this.createTime}(渲染前)`)
})
.onAppear(() => {
const renderCost = Date.now() - this.createTime
console.info(`[onAppear] 时间戳: ${Date.now()},渲染耗时约 ${renderCost}ms(渲染后)`)
})
.onDetach(() => {
console.info(`[onDetach] 组件存活时长: ${Date.now() - this.createTime}ms`)
})
}
}
三、可运行完整示例:四种事件综合演示
以下是结合官方示例设计的完整可运行演示页面,包含:
- 一键切换组件显隐的控制按钮
- 带有四个事件监听的目标组件(含 Toast 提示)
- 四个事件各自的触发次数统计面板
- 四种 API 说明对比卡片
- 可清除的实时事件日志(最新在顶)
typescript
// entry/src/main/ets/pages/Index.ets
import { promptAction } from '@kit.ArkUI'
@Entry
@Component
struct AppearExample {
@State isShow: boolean = true
@State appearCount: number = 0
@State disappearCount: number = 0
@State attachCount: number = 0
@State detachCount: number = 0
@State eventLog: string[] = ['等待显隐事件...']
private appendLog(msg: string): void {
this.eventLog = [msg, ...this.eventLog.slice(0, 11)]
}
build() {
Column({ space: 0 }) {
// ── 顶部标题栏 ──────────────────────────────────────
Row({ space: 10 }) {
Text('👁').fontSize(20)
Text('组件显隐事件演示').fontSize(17).fontWeight(FontWeight.Bold).fontColor(Color.White)
}
.width('100%').padding({ left: 20, right: 20, top: 14, bottom: 12 })
.backgroundColor('#1A237E').justifyContent(FlexAlign.Start)
Scroll() {
Column({ space: 16 }) {
// ── 控制按钮 ────────────────────────────────────
Button(this.isShow ? '隐藏组件(触发 onDisAppear)' : '显示组件(触发 onAppear)')
.width('90%').height(48).borderRadius(12)
.fontSize(14).fontWeight(FontWeight.Bold)
.backgroundColor(this.isShow ? '#C62828' : '#1B5E20')
.fontColor(Color.White)
.onClick(() => {
this.isShow = !this.isShow
})
// ── 被监听的组件(条件渲染触发 onAppear/onDisAppear) ──
if (this.isShow) {
Column({ space: 8 }) {
Text('👁 可见组件').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A237E')
Text('此组件挂载时触发 onAppear 和 onAttach').fontSize(12).fontColor('#555555')
Text('卸载时触发 onDisAppear 和 onDetach').fontSize(12).fontColor('#555555')
}
.width('90%').padding(18)
.backgroundColor('#E8EAF6').borderRadius(16)
.border({ width: 2, color: '#3F51B5', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Center)
// API 9+:组件挂载完成并渲染后触发
.onAppear(() => {
this.appearCount++
this.appendLog(`✅ onAppear 第 ${this.appearCount} 次`)
this.getUIContext().getPromptAction().showToast({
message: `onAppear 触发(第 ${this.appearCount} 次)`,
duration: 1500
})
})
// API 9+:组件从树中卸载或隐藏时触发
.onDisAppear(() => {
this.disappearCount++
this.appendLog(`○ onDisAppear 第 ${this.disappearCount} 次`)
this.getUIContext().getPromptAction().showToast({
message: `onDisAppear 触发(第 ${this.disappearCount} 次)`,
duration: 1500
})
})
// API 12+:组件挂载到组件树时触发(渲染前,禁止在此修改组件树)
.onAttach(() => {
this.attachCount++
this.appendLog(`🔗 onAttach 第 ${this.attachCount} 次`)
})
// API 12+:组件从组件树卸载时触发
.onDetach(() => {
this.detachCount++
this.appendLog(`🔌 onDetach 第 ${this.detachCount} 次`)
})
}
// ── 事件次数统计 ────────────────────────────────
Column({ space: 10 }) {
Text('📊 事件触发统计').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#1A237E').width('100%')
Row({ space: 0 }) {
Column({ space: 4 }) {
Text(`${this.appearCount}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1B5E20')
Text('onAppear').fontSize(10).fontColor('#9E9E9E')
}.layoutWeight(1)
Column().width(1).height(44).backgroundColor('#E0E0E0')
Column({ space: 4 }) {
Text(`${this.disappearCount}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#C62828')
Text('onDisAppear').fontSize(10).fontColor('#9E9E9E')
}.layoutWeight(1)
Column().width(1).height(44).backgroundColor('#E0E0E0')
Column({ space: 4 }) {
Text(`${this.attachCount}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1565C0')
Text('onAttach').fontSize(10).fontColor('#9E9E9E')
}.layoutWeight(1)
Column().width(1).height(44).backgroundColor('#E0E0E0')
Column({ space: 4 }) {
Text(`${this.detachCount}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#6A1B9A')
Text('onDetach').fontSize(10).fontColor('#9E9E9E')
}.layoutWeight(1)
}
.width('100%').padding({ top: 12, bottom: 12 })
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('90%').padding(14).backgroundColor(Color.White).borderRadius(14)
.border({ width: 1, color: '#C5CAE9', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start)
// ── 四种 API 说明卡片 ───────────────────────────
Column({ space: 8 }) {
Text('📌 API 说明').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#1A237E').width('100%')
ForEach([
['onAppear', 'API 9+', '#1B5E20', '组件挂载且渲染完成后触发,可安全操作布局'],
['onDisAppear', 'API 9+', '#C62828', '组件从树中卸载或隐藏时触发,适合做资源释放'],
['onAttach', 'API 12+', '#1565C0', '挂载到组件树时触发(渲染前),禁止在此修改组件树'],
['onDetach', 'API 12+', '#6A1B9A', '从组件树卸载时触发(渲染前),禁止在此修改组件树'],
], (row: string[]) => {
Row({ space: 10 }) {
Text(row[0]).fontSize(12).fontWeight(FontWeight.Bold).fontColor(row[2]).width(110)
Column({ space: 2 }) {
Text(row[1]).fontSize(10).fontColor('#888888')
Text(row[3]).fontSize(11).fontColor('#444444').width('100%')
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ top: 6, bottom: 6 })
.border({ width: { bottom: 1 }, color: '#EEEEEE', style: BorderStyle.Solid })
})
}
.width('90%').padding(14).backgroundColor('#F8F9FF').borderRadius(14)
.border({ width: 1, color: '#C5CAE9', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start)
// ── 事件日志 ────────────────────────────────────
Column({ space: 4 }) {
Row({ space: 8 }) {
Text('📋 事件日志(最新在顶)').fontSize(11).fontColor('#9E9E9E').layoutWeight(1)
Text('清除')
.fontSize(11).fontColor('#3F51B5')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(6)
.backgroundColor('#E8EAF6')
.onClick(() => {
this.eventLog = ['日志已清除']
})
}.width('100%')
ForEach(this.eventLog, (item: string, idx: number) => {
Text(item).fontSize(11)
.fontColor(idx === 0 ? '#1A237E' : '#BDBDBD')
.width('100%')
})
}
.width('90%').padding(12).backgroundColor('#F9F9F9').borderRadius(12)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start)
}
.width('100%').alignItems(HorizontalAlign.Center).padding({ top: 16, bottom: 32 })
}
.layoutWeight(1).width('100%')
}
.width('100%').height('100%').backgroundColor('#FAFAFA')
}
}
如图

四、实战场景:典型业务用法
4.1 onAppear 驱动数据懒加载
在列表滚动中,onAppear 是实现图片懒加载 和数据分页的理想时机:
typescript
@Component
struct LazyImageCard {
@State loaded: boolean = false
@State imgSrc: string = ''
url: string = ''
title: string = ''
build() {
Column({ space: 8 }) {
if (this.loaded) {
Image(this.imgSrc).width('100%').height(120).borderRadius(8).objectFit(ImageFit.Cover)
} else {
// 占位图
Column() {
Text('加载中...').fontSize(12).fontColor('#9E9E9E')
}
.width('100%').height(120).backgroundColor('#F5F5F5').borderRadius(8)
.justifyContent(FlexAlign.Center)
}
Text(this.title).fontSize(13).fontColor('#333333').width('100%')
}
.padding(12).backgroundColor(Color.White).borderRadius(12)
// 进入可见区域时才加载真实图片
.onAppear(() => {
if (!this.loaded) {
// 模拟异步加载
this.imgSrc = this.url
this.loaded = true
console.info(`[懒加载] 图片已加载:${this.title}`)
}
})
}
}
4.2 onDisAppear 暂停/恢复动画
视频缩略图、Lottie 动画等组件在不可见时应暂停播放,回到可见时恢复:
typescript
@Component
struct AnimatedBanner {
@State isPlaying: boolean = false
private animatorTimer: number = -1
build() {
Column() {
Text(this.isPlaying ? '🎬 动画播放中...' : '⏸ 动画已暂停')
.fontSize(14).fontColor(this.isPlaying ? '#1B5E20' : '#9E9E9E')
}
.width('100%').height(80).backgroundColor('#E8F5E9').borderRadius(12)
.justifyContent(FlexAlign.Center)
.onAppear(() => {
// 组件可见时启动动画
this.isPlaying = true
console.info('[onAppear] 动画开始播放')
})
.onDisAppear(() => {
// 组件不可见时暂停动画,节省性能
this.isPlaying = false
console.info('[onDisAppear] 动画已暂停,释放资源')
})
}
}
4.3 onAttach / onDetach 管理事件监听
onAttach 注册监听,onDetach 注销监听,形成完整生命周期闭环,防止内存泄漏:
typescript
@Component
struct SensorComponent {
@State sensorValue: number = 0
private listenerId: number = -1
build() {
Column({ space: 8 }) {
Text(`传感器值:${this.sensorValue}`).fontSize(14).fontColor('#333333')
Text('onAttach 注册监听,onDetach 注销监听').fontSize(11).fontColor('#9E9E9E')
}
.padding(14).backgroundColor('#FFF8E1').borderRadius(12)
// 挂载时注册(渲染前,不修改组件树)
.onAttach(() => {
this.listenerId = Date.now() // 模拟注册传感器监听
console.info(`[onAttach] 传感器监听已注册,listenerId=${this.listenerId}`)
})
// 卸载时注销(渲染前,不修改组件树)
.onDetach(() => {
console.info(`[onDetach] 传感器监听已注销,listenerId=${this.listenerId}`)
this.listenerId = -1
})
}
}
五、与组件自定义生命周期的关系
ArkUI 组件同时拥有自定义组件生命周期 (aboutToAppear/aboutToDisappear)和通用显隐事件 (onAppear/onDisAppear/onAttach/onDetach),两者在触发时机和用途上有明显区别:
| 对比项 | aboutToAppear |
onAppear |
onAttach |
|---|---|---|---|
| 所属类型 | 自定义组件生命周期 | 通用事件 | 通用事件 |
| 触发时机 | build 函数执行前 | 渲染完成可见后 | 插入组件树时(渲染前) |
| 可挂载到 | 自定义 @Component |
任意组件 | 任意组件 |
| 触发次数 | 仅一次 | 可重复 | 仅一次 |
| 适合场景 | 组件级初始化 | 入场动画、数据加载 | 系统监听注册 |
typescript
@Component
struct LifecycleDemo {
// 自定义组件生命周期:build 前调用,仅一次
aboutToAppear(): void {
console.info('[aboutToAppear] build 函数即将执行')
}
build() {
Text('生命周期对比')
.padding(16)
// 通用显隐事件:渲染完成后调用,可重复
.onAppear(() => {
console.info('[onAppear] 渲染完成,组件可见')
})
// 通用挂载事件:插入树时调用,仅一次(渲染前)
.onAttach(() => {
console.info('[onAttach] 已插入组件树')
})
}
}
总结
本文系统讲解了 HarmonyOS ArkUI 组件显隐事件 的完整知识体系,核心要点回顾:
- 四种 API 各司其职 :
onAttach(仅一次,渲染前)→onAppear(可重复,渲染后)→onDisAppear(可重复,渲染后)→onDetach(仅一次,渲染前),形成完整的组件可见性生命周期 - 渲染前 vs 渲染后是核心区别 :
onAttach/onDetach在渲染前触发,严禁修改组件树 ,只做非 UI 操作;onAppear/onDisAppear在渲染后触发,可安全驱动动画、加载数据、更新 State - onAppear 会重复触发 :页面每次从后台切回、路由返回当前页,都会再次触发
onAppear,需配合initialized标志防止数据重复加载 - 与 onDetach 配对使用防泄漏 :凡是在
onAttach中注册的监听器、定时器,必须在onDetach中对应注销 - 业务选型优先考虑 onAppear :绝大多数业务场景(懒加载、动画、埋点)使用
onAppear/onDisAppear即可,onAttach/onDetach用于需要精确单次触发的系统资源管理
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!