在 HarmonyOS 开发中,setTimeout 和 setInterval 是处理延迟操作和周期性任务的核心 API。它们分别适用于单次延迟和重复执行场景,但在实际使用中,必须严格遵循生命周期管理和防内存泄漏的最佳实践。

一、 核心区别与基础用法

特性 setTimeout setInterval
执行次数 仅执行一次(延迟后触发) 多次重复执行(按固定间隔触发)
终止方式 clearTimeout(timeoutID) clearInterval(intervalID)
适用场景 延迟跳转、防抖、单次状态更新 倒计时、轮播图、定时刷新数据
时间准确性 延迟时间是“至少”等待的时间 间隔时间可能因主线程繁忙被拉长
1. 单次延迟操作(setTimeout)
// 延迟 1 秒后执行
let timeoutID = setTimeout(() => {
  console.info('延迟 1s 后执行');
}, 1000);

// 取消定时器(如果在 1s 内触发)
clearTimeout(timeoutID);
2. 周期性重复操作(setInterval)
// 每 3 秒执行一次
let intervalID = setInterval((param1, param2) => {
  console.info('周期执行:', param1, param2);
}, 3000, '参数1', '参数2');

// 取消定时器
clearInterval(intervalID);

二、 核心避坑指南:生命周期与内存泄漏

在组件化开发中,忘记清理定时器是导致内存泄漏和状态错乱的最常见原因。如果用户提前关闭了页面,而定时器仍在后台运行,可能会引发应用崩溃或电量异常消耗。

1. 必须在组件销毁时清理定时器

在 ArkUI 中,必须在 aboutToDisappear 生命周期中清除所有定时器:

@State timerId: number = -1;
@State progressTimerId: number = -1;

aboutToDisappear(): void {
  // 清理 setTimeout
  if (this.timerId !== -1) {
    clearTimeout(this.timerId);
    this.timerId = -1;
  }
  // 清理 setInterval
  if (this.progressTimerId !== -1) {
    clearInterval(this.progressTimerId);
    this.progressTimerId = -1;
  }
}
2. 防止重复触发与状态错乱(幂等保护)

在执行定时任务的目标函数时,建议加入状态锁(如 jumped 标志位),防止定时器多次触发导致的逻辑重复执行:

@State jumped: boolean = false;

goHome(): void {
  if (this.jumped) return; // 幂等保护,防止重复跳转
  this.jumped = true;
  
  // 跳转前主动清理定时器
  if (this.timerId !== -1) clearTimeout(this.timerId);
  if (this.progressTimerId !== -1) clearInterval(this.progressTimerId);
  
  // 执行页面跳转逻辑...
}

三、 高级场景:避免累积延迟

由于 JavaScript/ArkTS 是单线程模型,当主线程繁忙时,setInterval 可能会出现“前一次回调尚未执行完,下一次回调已经触发”的累积延迟问题。

最佳实践:对于需要严格保证执行顺序的周期性任务,推荐使用 setTimeout 递归调用来替代 setInterval

const runTask = () => {
  console.info('执行周期性任务');
  // 确保当前任务执行完毕后,再设定下一次执行
  setTimeout(runTask, 1000); 
};
runTask(); // 启动任务

四、 特殊场景:后台冻结与数据持久化

1. 后台冻结机制

在 HarmonyOS 中,如果 UI 界面退到后台,受底层原理管控,定时器会被系统冻结。当应用切回前台时,定时器才会恢复执行。这意味着依赖 setInterval 计数的倒计时,在后台期间会“暂停”。

2. 跨后台的精准倒计时方案

如果需要实现跨后台依然精准的倒计时(如秒杀、验证码),不能单纯依赖定时器累加,而应采用“时间戳比对 + 数据持久化”的方案:

  1. 启动倒计时时,将 开始时间 和 结束时间 存入用户首选项(Preferences)。
  2. 使用 setInterval 仅作为 UI 刷新触发器(如每秒刷新一次)。
  3. 每次触发时,通过 当前时间 - 结束时间 计算剩余时间。
  4. 在 onPageHide 时保存剩余时间,在 onPageShow 时读取并恢复状态。

这样即使应用进程被杀或长时间处于后台,重新打开时依然能展示正确的剩余时间。

五、 复杂业务场景:弹窗自动关闭与拦截

在社交通讯或提示类场景中,常需要实现“弹窗倒计时自动关闭”且“防止用户误触关闭”的功能。这需要将定时器与自定义弹窗的生命周期和拦截回调深度结合。

核心实现思路

  1. 在弹窗组件的 aboutToAppear 生命周期中启动 setInterval 倒计时。
  2. 利用 onWillDismiss 回调拦截用户的关闭操作(如点击遮罩层、按返回键)。
  3. 倒计时结束或用户手动点击关闭时,调用 clearInterval 清理任务并关闭弹窗。
@CustomDialog
@Component
struct AutoCloseCustomDialog {
  @State timeout: number = 10;
  private interval: number = -1;
  controller?: CustomDialogController;

  aboutToAppear(): void {
    // 启动倒计时
    this.interval = setInterval(() => {
      if (this.timeout <= 1) {
        this.closeDialog(); // 倒计时结束自动关闭
        return;
      }
      this.timeout -= 1;
    }, 1000);
  }

  closeDialog(): void {
    clearInterval(this.interval); // 必须清理定时器
    this.controller?.close();
  }

  build() {
    Column() {
      Text(`自动关闭倒计时: ${this.timeout}s`)
      Button('手动关闭').onClick(() => this.closeDialog())
    }
  }
}

// 在父组件中拦截用户关闭操作
CustomDialogController({
  builder: AutoCloseCustomDialog(),
  onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
    // 拦截返回键或点击遮罩层关闭
    if (dismissDialogAction.reason === DismissReason.PRESS_BACK || 
        dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) {
      // 阻止默认关闭行为(根据业务需求决定是否允许关闭)
    }
  }
})

六、 跨线程任务调度:定时器 + Taskpool

当定时任务中涉及耗时的计算或数据处理时,直接在主线程执行会导致 UI 卡顿。最佳实践是将定时器作为触发器,将实际业务逻辑交由 Taskpool 多线程处理

import { taskpool } from '@kit.ArkTS';

// 耗时任务必须加上 @Concurrent 装饰器
@Concurrent
function heavyDataProcess(pars: number): number {
  // 模拟耗时计算
  let result = 0;
  for (let i = 0; i < pars; i++) result += i;
  return result;
}

// 定时器触发器
let timerId = setInterval(async () => {
  const task = new taskpool.Task(heavyDataProcess, 1000000);
  // 将耗时任务放入高优先级线程池执行
  const result = await taskpool.execute(task, taskpool.Priority.HIGH);
  console.info('耗时任务计算结果: ' + result);
}, 3000);

七、 精准倒计时完整代码实例(持久化方案)

针对前文提到的“跨后台精准倒计时”,以下提供完整的状态管理与持久化代码实现:

import { preferences } from '@kit.ArkData';

@Entry
@Component
struct PreciseCountdown {
  @State message: number = 60;
  private timerId: number = -1;

  aboutToAppear(): void {
    this.startCountdown();
  }

  startCountdown(): void {
    if (this.timerId !== -1 || this.message <= 0) return;
    this.timerId = setInterval(() => {
      if (this.message > 0) {
        this.message--;
      } else {
        clearInterval(this.timerId);
        this.timerId = -1;
      }
    }, 1000);
  }

  // 页面隐藏时,将结束时间持久化
  onPageHide(): void {
    const endTime = Date.now() + this.message * 1000;
    // 存入 Preferences: dataPreferences.putSync('endTime', endTime);
  }

  // 页面重新显示时,根据时间戳差值恢复状态
  onPageShow(): void {
    // 从 Preferences 读取 endTime
    // const remainingTime = Math.floor((endTime - Date.now()) / 1000);
    // this.message = remainingTime > 0 ? remainingTime : 0;
    // this.startCountdown();
  }

  aboutToDisappear(): void {
    if (this.timerId !== -1) clearInterval(this.timerId);
  }
}

八、 H5 混合开发:Webview 后台冻结处理

当应用内嵌 H5 页面时,H5 中的 JS 定时器同样会受到后台冻结机制的影响。如果需要在后台保持 H5 倒计时,需要在原生侧申请短时任务(ShortTimeTask)

解决方案

  1. 监听应用前后台切换事件。
  2. 进入后台时,通过 backgroundTaskManager 申请短时任务,延长应用在后台的存活时间。
  3. 回到前台时,主动取消短时任务,释放系统资源。
import { backgroundTaskManager } from '@kit.BasicServicesKit';

// 进入后台时申请短时任务
let shortTimeTaskId = -1;
try {
  shortTimeTaskId = backgroundTaskManager.startBackgroundRunning(
    'H5CountdownTask', 
    'H5 page needs to run in background',
    { isProcess: true }
  );
} catch (err) {
  console.error('申请短时任务失败');
}

// 回到前台时取消短时任务
if (shortTimeTaskId !== -1) {
  backgroundTaskManager.stopBackgroundRunning(shortTimeTaskId);
}

九、 系统级后台调度:延期任务(WorkScheduler)

普通的定时器在应用退到后台后会被冻结,而延期任务(WorkSchedulerExtensionAbility)允许应用在满足特定条件时(如连接 Wi-Fi、电池电量低于 20%、正在无线充电等)自动在后台执行任务。这非常适合定期数据同步、大文件下载等场景。

核心实现步骤

  1. 在 module.json5 中注册 WorkSchedulerExtensionAbility,并设置 type 为 workScheduler
  2. 实现 onWorkStart 和 onWorkStop 生命周期回调。
  3. 通过 workScheduler.startWork(workInfo) 提交任务及触发条件。
import { workScheduler } from '@kit.BackgroundTasksKit';

// 定义触发条件:在连接 Wi-Fi 且电池电量低于 20% 时执行
const workInfo: workScheduler.WorkInfo = {
  workId: 1,
  networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,
  bundleName: 'com.example.application',
  abilityName: 'MyWorkSchedulerExtensionAbility',
  batteryLevel: 20, 
  chargerType: workScheduler.ChargingType.CHARGER_TYPE_WIRELESS 
};

try {
  workScheduler.startWork(workInfo);
  console.info('延期任务申请成功');
} catch (error) {
  console.error('延期任务申请失败');
}

十、 跨后台的精准通知:代理提醒(Reminder Agent)

如果业务场景是“倒计时结束”或“特定日历时间”需要提醒用户(如闹钟、日程),即使应用进程被终止,系统依然能弹出通知。这需要使用代理提醒能力。

核心实现步骤

  1. 在 AppGallery Connect 申请“代理提醒”开放能力,并申请 ohos.permission.PUBLISH_AGENT_REMINDER 权限。
  2. 请求用户的通知授权。
  3. 定义提醒代理(如倒计时类型 ReminderRequestTimer)。
import { reminderAgentManager } from '@kit.BackgroundTasksKit';
import { notificationManager } from '@kit.NotificationKit';

// 定义一个倒计时提醒代理
let timer: reminderAgentManager.ReminderRequestTimer = {
  reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER,
  ringDuration: 5, // 响铃时长(秒)
  title: '计时器提醒',
  wantAgent: { 
    pkgName: 'com.example.reminderagentmanager',
    abilityName: 'EntryAbility'
  },
  notificationId: 100,
  slotType: notificationManager.SlotType.CONTENT_INFORMATION,
  triggerTimeInSeconds: 60 // 60秒后触发
};

// 发布代理提醒
reminderAgentManager.publishReminder(timer).then((reminderId) => {
  console.info('代理提醒发布成功, ID: ' + reminderId);
});

十一、 复杂任务编排:串行队列(SequenceRunner)

在实际业务中,经常需要处理多个具有先后依赖关系的异步任务(如:先清理缓存 -> 再拉取配置 -> 最后渲染 UI)。ArkTS 提供了 SequenceRunner 串行队列,确保任务按顺序执行,避免多线程竞争和乱序。

import { taskpool } from '@kit.ArkTS';

// 1. 定义需要串行执行的任务
@Concurrent
function step1_ClearCache(): string {
  // 模拟清理缓存耗时
  return 'Cache Cleared';
}

@Concurrent
function step2_FetchConfig(): string {
  // 模拟拉取配置耗时
  return 'Config Fetched';
}

// 2. 创建串行队列并按顺序添加任务
let runner = new taskpool.SequenceRunner();
runner.addTask(new taskpool.Task(step1_ClearCache));
runner.addTask(new taskpool.Task(step2_FetchConfig));

// 3. 启动串行队列
runner.execute().then(() => {
  console.info('所有串行任务执行完毕');
});

十二、 长时监听任务:TaskPool 的 LongTask

对于非驻留但需要长时间不间断运行的独立任务(如持续监听某个硬件传感器事件、长连接心跳),不建议使用 Worker,而推荐使用 TaskPool 提供的 LongTask。它运行周期长,但每次执行不会长时间阻塞线程,资源消耗更低。

import { taskpool } from '@kit.ArkTS';

@Concurrent
async function longListenTask() {
  try {
    // 持续监听事件或进行长连接
    while(true) {
      // 模拟向主线程发送监听到的数据
      taskpool.Task.sendData({ event: 'sensor_data', value: 100 });
      // 避免死循环阻塞,适当休眠
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  } catch (err) {
    console.error('长时任务异常');
  }
}

// 启动长时任务并接收数据
let longTask = new taskpool.LongTask(longListenTask);
longTask.onReceiveData((msg: Object) => {
  console.info('收到长时任务数据: ' + JSON.stringify(msg));
});

taskpool.execute(longTask);
Logo

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

更多推荐