HarmonyOS6 组件显隐事件(onAppear / onDisAppear / onAttach / onDetach)

文章目录

一、API 接口详解

1.1 onAppear

typescript 复制代码
// 接口签名(API 9+)
onAppear(event: () => void): T
参数 类型 说明
event () => void 组件进入可见状态时的回调,无参数
返回值 当前组件实例 T 支持链式调用

触发场景:

  1. 组件首次渲染完成,进入屏幕可见区域
  2. 页面从后台切换到前台(App 切回)
  3. 路由从其他页面返回到当前页面

1.2 onDisAppear

typescript 复制代码
// 接口签名(API 9+)
onDisAppear(event: () => void): T

触发场景:

  1. 条件渲染 if (isShow)isShow 变为 false,组件被移除
  2. 页面切换到后台(App 切走)
  3. 路由跳转到其他页面

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`)
    })
  }
}

三、可运行完整示例:四种事件综合演示

以下是结合官方示例设计的完整可运行演示页面,包含:

  1. 一键切换组件显隐的控制按钮
  2. 带有四个事件监听的目标组件(含 Toast 提示)
  3. 四个事件各自的触发次数统计面板
  4. 四种 API 说明对比卡片
  5. 可清除的实时事件日志(最新在顶)
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 组件显隐事件 的完整知识体系,核心要点回顾:

  1. 四种 API 各司其职onAttach(仅一次,渲染前)→ onAppear(可重复,渲染后)→ onDisAppear(可重复,渲染后)→ onDetach(仅一次,渲染前),形成完整的组件可见性生命周期
  2. 渲染前 vs 渲染后是核心区别onAttach/onDetach 在渲染前触发,严禁修改组件树 ,只做非 UI 操作;onAppear/onDisAppear 在渲染后触发,可安全驱动动画、加载数据、更新 State
  3. onAppear 会重复触发 :页面每次从后台切回、路由返回当前页,都会再次触发 onAppear,需配合 initialized 标志防止数据重复加载
  4. 与 onDetach 配对使用防泄漏 :凡是在 onAttach 中注册的监听器、定时器,必须在 onDetach 中对应注销
  5. 业务选型优先考虑 onAppear :绝大多数业务场景(懒加载、动画、埋点)使用 onAppear/onDisAppear 即可,onAttach/onDetach 用于需要精确单次触发的系统资源管理

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关推荐
是稻香啊5 小时前
HarmonyOS6 ArkUI 组件尺寸变化事件(onSizeChange)全面解析与实战演示
harmonyos6
ITUnicorn21 天前
【HarmonyOS 6】进度组件实战:打造精美的数据可视化
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn25 天前
【HarmonyOS 6】数据可视化:实现热力图时间块展示
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS 6】HarmonyOS 自定义时间选择器实现
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】ArkTS 自定义组件封装实战:动画水杯组件
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】从零实现随机数生成器
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】从零实现自定义计时器:掌握TextTimer组件与计时控制
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】简易计数器开发
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
Flutter x HarmonyOS 6:依托小艺开放平台创建智能体并在应用中调用
flutter·harmonyos·鸿蒙·智能体·harmonyos6