23 — @Monitor 与 @Computed:计算与监听

一、引言

在状态管理 V2 中,@Trace 解决了"数据可观察"的问题,但"数据变化后要做什么"仍需要一种显式机制:@Monitor 监听 @Trace 属性变化并触发业务逻辑,@Computed 提供带缓存的派生计算属性。本项目播放器组件 features/multishortvideoadaptivevideo/src/main/ets/view/AdaptiveAVPlayer.ets 用三个 @Monitor 串起了"滑动切换视频、窗口尺寸变化、拖动进度条"三条关键联动链路,是理解 V2 监听机制的绝佳样本。本文结合该文件逐一拆解 @Monitor 的用法与 @Computed 的适用场景。
在这里插入图片描述

二、@Monitor 基本用法

@Monitor 装饰方法,监听一个或多个 @Trace(或 @Local/@Param 等)属性,属性变化时自动调用。AdaptiveAVPlayer.ets 中第一个 @Monitor 处理"当前播放下标变化":

// features/multishortvideoadaptivevideo/src/main/ets/view/AdaptiveAVPlayer.ets(真实代码)
@Monitor('currentIndex')
onIndexChange() {
  if (this.currentIndex !== this.index) {
    // 非当前项:暂停播放,节省资源
    this.avPlayer?.pause().catch(() => {
      Logger.error(TAG, 'Pause AVPlayer failed.');
    })
  } else {
    this.changePortraitVideo();
    if (this.hasPrepared) {
      if (this.canPlay()) {
        this.avPlayer?.seek(0, media.SeekMode.SEEK_CLOSEST);
        this.avPlayer?.play().catch(() => {
          Logger.error(TAG, 'Play AVPlayer failed');
        })
      }
    } else {
      this.initAVPlayer()
    }
  }
}

当前后滑动手势切换视频时,父组件 AdaptiveVideo 在 Swiper 的 onAnimationStart 中更新 this.curIndex 并传给播放器的 @Param currentIndex,@Monitor 随即被触发:非当前页暂停、当前页回到起点并播放或初始化。业务逻辑与 UI 刷新解耦,且天然携带"哪个属性变了"的上下文。

三、@Monitor 多属性监听与完整方法签名

@Monitor 支持一次监听多个属性,AdaptiveAVPlayer.ets 中窗口尺寸联动即如此:

// 真实代码:窗口宽高任一变化都重算视频显示区域
@Monitor('windowSizeWidth', 'windowSizeHeight')
onWindowSizeChange() {
  this.changePortraitVideo();
}

@Monitor 方法还可接收 IMonitorChange 参数,获取变化前后的值:

@Monitor('seekToTime')
onSeekToTimeChange(change: IMonitorChange) {
  const oldValue = change.value?.['seekToTime'] as number;   // 变化前
  const newValue = change.changeValue?.['seekToTime'] as number; // 变化后
  if (this.seekToTime >= 0 && this.avPlayer && this.canSeek()) {
    this.avPlayer.seek(Math.floor(this.seekToTime), media.SeekMode.SEEK_CLOSEST);
  }
}

change.value 记录属性变化前的值,change.changeValue 记录变化后的值,可用于差值计算、埋点上报或"恢复现场"(如拖拽进度时记住原进度,取消拖拽可回退)。IMonitorChange 的结构可归纳为:

字段含义
value变化前各监听属性的值集合
changeValue变化后各监听属性的值集合
keys本次实际变化的属性名数组

keys 在多属性监听时尤其有用:即使同时监听宽高,也能通过 keys 判断这次是宽度变了还是高度变了,从而决定是否触发重排。项目中的 @Monitor('seekToTime') 正是拖动进度条的核心链路:用户在进度条上拖动 → 父组件 AdaptiveVideo 通过 updateSeekByPosition 计算目标时间并更新 seekToTime → @Monitor 触发 seek 跳转,配合 isSeeking 标志位区分"拖拽预览"与"播放推进",从而实现了"拖到哪播到哪"的实时预览。

四、@Computed:带缓存的派生属性

@Computed 装饰 getter 方法,基于 @Trace 属性派生出新值,且只在依赖项变化时重新计算,结果自动缓存。典型场景是把播放进度换算为进度条百分比或时间文本:

// 扩展示例:基于项目 currentTime/duration 的派生计算(项目风格)
@ComponentV2
struct AdaptiveVideo {
  @Consumer('currentTime') currentTime: number = 0;
  @Consumer('duration') duration: number = 0;

  @Computed
  get progressPercent(): number {
    if (this.duration <= 0) {
      return 0;
    }
    return Math.min(100, (this.currentTime / this.duration) * 100);
  }

  @Computed
  get timeText(): string {
    const total = Math.floor(this.duration / 1000);
    const cur = Math.floor(this.currentTime / 1000);
    const format = (sec: number): string =>
      `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`;
    return `${format(cur)} / ${format(total)}`;
  }
}

与每次渲染都执行计算的普通 getter 不同,@Computed 只在依赖的 @Trace 属性变化时重算,timeUpdate 每 500ms 触发一次 currentTime 更新时,只有真正变化才触发重算,避免高频渲染下的无谓计算。派生逻辑集中收口在组件内,也提升了可读性。

需要说明的是,@Computed 只读不可写,若需要"百分比 → 目标时间"的反向换算(拖动进度条时把手指位置换算为 seekToTime),仍应写在普通方法中(项目 AdaptiveVideo.ets 的 updateSeekByPosition 即为此类),单向数据流下计算与赋值各司其职。

五、@Monitor 与 @Watch 对比

维度V1 @WatchV2 @Monitor
获取变化前后值不支持支持(IMonitorChange)
多属性监听单属性支持,可监听多个
依赖的观察体系@State 等 V1@Trace 等 V2 属性
触发时机属性变化后属性变化后(异步批量)
应用场景简单回调联动播放、窗口重算、持久化

@Monitor 还具备去重与批量能力:同一帧内多次修改被观察属性,方法只会执行一次,天然适合合并窗口 resize 这类高频事件。AdaptiveAVPlayer.ets 中窗口尺寸监听只做一次重算,正是受益于此。

此外,@Monitor 对 @Local、@Param、@Consumer 等 V2 装饰器属性同样生效:AdaptiveAVPlayer.ets 中 currentIndex 是 @Param(父组件传入)、seekToTime 是 @Param、windowSizeWidth/windowSizeHeight 是 @Consumer,三种来源的属性变化都能触发对应监听,说明 @Monitor 与"状态从哪来"无关,只关心"属性值是否变化"。

六、使用注意事项

  1. @Monitor 不能修改被监听的属性本身,否则会形成递归触发,应通过其他状态或事件回传。
  2. 方法体内优先读取变化后的最新值,如需历史值用 IMonitorChange 的 change.value。
  3. @Computed 依赖的必须是 @Trace/@Local 等可观察属性,普通成员变量变化不会触发重算。
  4. 播放器回调(onStateChange、onTimeUpdateFunction)中直接赋值 @Consumer 属性,配合 @Monitor 联动 UI,注意不要在 @Monitor 中做耗时操作。
  5. 组件销毁时 @Monitor 自动解绑,无需手动注销,但播放器等原生资源仍需在 aboutToDisappear 中显式释放。

七、总结与最佳实践

  1. 用 @Monitor 把"状态变化"与"业务副作用"显式关联:切视频暂停/起播、窗口变化重排、进度跳转,都收敛为一条清晰的联动链路。
  2. 多属性联动的场景(宽高、坐标)一次 @Monitor 监听多个属性,利用批量去重天然抗抖。
  3. 需要变化前后值(拖拽回退、差值统计)时使用 IMonitorChange 参数。
  4. 派生展示值(百分比、时间文本、格式化文案)用 @Computed,享受缓存的同时保持代码整洁。
  5. 坚持单向数据流:@Monitor 只做副作用,UI 刷新交给 @Trace 自身完成,二者职责不重叠。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐