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 用于需要精确单次触发的系统资源管理

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

相关推荐
全栈若城1 天前
HarmonyOS6 半年磨一剑 - RcInput 组件清空、密码切换与图标交互机制
架构·交互·harmonyos6·三方库开发实战·rchoui·三方库开发
全栈若城6 天前
HarmonyOS 6 实战:Component3D 与 SURFACE 渲染模式深度解析
3d·架构·harmonyos6
全栈若城6 天前
HarmonyOS 6 实战:使用 ArkGraphics3D 加载 GLB 模型与 Scene 初始化全流程
3d·华为·架构·harmonyos·harmonyos6
是稻香啊14 天前
HarmonyOS6 ArkTS Popup 气泡组件指南
harmonyos6
是稻香啊14 天前
HarmonyOS6 触摸目标 touch-target 属性使用指南
harmonyos6
是稻香啊15 天前
HarmonyOS6 foregroundBlurStyle 通用属性使用指南
harmonyos6
是稻香啊16 天前
HarmonyOS6 clickEffect 通用属性使用指南
harmonyos6
是稻香啊16 天前
HarmonyOS6 filter 通用属性使用指南
harmonyos6
是稻香啊21 天前
HarmonyOS6 ArkUI 无障碍悬停事件(onAccessibilityHover)全面解析与实战演示
华为·harmonyos·harmonyos6
是稻香啊22 天前
HarmonyOS6 背景设置:background 基础属性全解析
harmonyos6