需求
- 参数要求可传入倒计时间,获取倒计时步长(多久获取一次当前剩余倒计时)
- 要求监听倒计时时间
- 要求可以调用启动倒计时,暂停倒计时,重置倒计时
准备工作
- 使用eventemitter3提供事件监听的能力
- 准备返回值的 parseRemainDate 方法,用以格式化返回值
- 准备start,stop,reset方法,用于操作定时器
- 准备countDown方法,用于倒计时主要逻辑
倒计时原理
由这个图我们得知
- 首先我们要规定好 倒计时运行的时间范围
[lastTimeStamp, endTimeStamp],中间的区间范围差值就为初始的倒计时时间,随着时间不断推移,currentTimeStamp会向右移动(时间戳增加),因此就会不断减少剩余倒计时 - 假如我们在
[lastTimeStamp, endTimeStamp]中的任意一个位置暂停了,那么下一次我们恢复倒计时的时候,倒计时的运行范围就应该在[第二次开始运行的时间点, 第二次开始运行时间点 + 第一次剩余的倒计时时间]
倒计时实现
类型声明
// 提供事件监听能力
import { EventEmitter } from "eventemitter3";
// 计时器内部状态
enum CountDownStatus {
running,
stopped,
}
// 事件枚举
export enum CountDownEventName {
START = "start",
STOP = "stop",
RUNNING = "running",
RESET = "reset",
INIT = "init",
}
// 定义监听事件返回值
export type RemainTimeData = {
days: number;
hours: number;
minutes: number;
seconds: number;
counts: number;
};
// 监听事件,以及其回调值
interface CountdownEventMap {
[CountDownEventName.START]: [];
[CountDownEventName.STOP]: [];
[CountDownEventName.RESET]: [RemainTimeData];
[CountDownEventName.RUNNING]: [RemainTimeData];
[CountDownEventName.INIT]: [RemainTimeData];
}
变量初始化
export default class CountDown extends EventEmitter<CountdownEventMap> {
// 基于毫秒时间,算出其他时间单位和毫秒的关系,方便将后面的时间戳转换成需要暴露的格式
private static COUNT_IN_MILLSECOND: number = 1 * 100; // 1毫秒
private static SECOND_IN_MILLSECOND: number = CountDown.COUNT_IN_MILLSECOND * 10; // 1秒
private static MINUTE_IN_MILLSECOND: number = CountDown.SECOND_IN_MILLSECOND * 60; // 1分钟
private static HOUR_IN_MILLSECOND: number = CountDown.MINUTE_IN_MILLSECOND * 60; // 1小时
private static DAY_IN_MILLSECOND: number = CountDown.HOUR_IN_MILLSECOND * 24; // 1天
private lastTimeStamp: number; // 上一次开始计时的时间戳
private endTimeStamp: number; // 结束时的时间戳
private countDownTime: number; // 倒计时间
private remainTime: number; // 当前剩余倒计时秒数
private step: number;
private timer: number;
private status: CountDownStatus;
constructor(countDownTime: number, step = 1) {
// 传入的单位都为秒,需要都转换为毫秒初始值
super();
// 初始化
this.lastTimeStamp = Date.now(); // 上一次倒计时的时间戳
this.countDownTime = countDownTime * 1000; // 初始化倒计时长
this.remainTime = this.countDownTime; // 初始化当前剩余时间
this.endTimeStamp = this.lastTimeStamp + this.countDownTime; // 初始化结束时间戳
this.step = step * 1000; // 定时器步长
this.status = CountDownStatus.stopped; // 定时器状态
this.timer = setTimeout(() => {
// 展示初始的倒计时,并在监听事件的回调函数中获取初始值
this.emit(CountDownEventName.INIT, this.parseRemainDate(this.remainTime));
}, 0);
}
}
操作倒计时的方法
// 开启倒计时
public start() {
this.status = CountDownStatus.running;
// 创建倒计时本次运行的区间范围,并执行倒计时处理
this.lastTimeStamp = Date.now();
this.endTimeStamp = this.lastTimeStamp + this.remainTime;
this.countDown();
}
// 停止倒计时
public stop() {
// 改变计时器状态,停止倒计时
this.status = CountDownStatus.stopped;
this.emit(CountDownEventName.STOP);
clearTimeout(this.timer);
}
// 重置倒计时
public reset() {
if (this.status === CountDownStatus.stopped) {
this.emit(
CountDownEventName.RESET,
this.parseRemainDate(this.countDownTime)
);
}
}
处理倒计时的核心方法 - countDown
private countDown() {
// 若倒计时正在运行则不处理
if (this.status !== CountDownStatus.running) return;
// 通过结束时间和currentTimeStamp的差值,获取当前剩余的倒计时时间
const currentTimeStamp = Date.now()
this.remainTime = Math.max(this.endTimeStamp - currentTimeStamp, 0);
this.emit(
CountDownEventName.RUNNING,
this.parseRemainDate(this.remainTime)
);
if (this.remainTime > 0) {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.countDown();
}, this.step);
} else {
// 倒计时结束
this.stop();
}
}
格式化剩余倒计时
private parseRemainDate(remainTime: number): RemainTimeData {
let time = remainTime;
const days = Math.floor(time / CountDown.DAY_IN_MILLSECOND);
time = time % CountDown.DAY_IN_MILLSECOND;
const hours = Math.floor(time / CountDown.HOUR_IN_MILLSECOND);
time = time % CountDown.HOUR_IN_MILLSECOND;
const minutes = Math.floor(time / CountDown.MINUTE_IN_MILLSECOND);
time = time % CountDown.MINUTE_IN_MILLSECOND;
const seconds = Math.floor(time / CountDown.SECOND_IN_MILLSECOND);
time = time % CountDown.SECOND_IN_MILLSECOND;
const counts = Math.floor(time / CountDown.COUNT_IN_MILLSECOND);
return {
days,
hours,
minutes,
seconds,
counts,
};
}
倒计时使用
// 实例化倒计时对象
const countDownTime = new MyCountDown(360, 0.1); // 360秒倒计时,并且每0.1秒给与剩余倒计时长
// 监听初始事件
countDownRef.current.on(CountDownEventName.INIT, (remainTimeData: RemainTimeData) => {
// ...
});
// 监听重置事件
countDownRef.current.on(CountDownEventName.RESET, (remainTimeData: RemainTimeData) => {
// ...
});
// 监听运行事件
countDownRef.current.on(CountDownEventName.RUNNING, (remainTimeData:RemainTimeData) => {
// ...
});
注意
因为浏览器中的JS线程和GUI线程是互为阻塞的,因此,使用setTimeout可能会因为复杂渲染任务的阻塞导致其结果存在误差。
- 可以参考使用web worker的方式,通过启动单独的线程处理倒计时任务
- 当然也可以使用
requestAnimationFrame替代setTimeout来处理倒计时计算,这是因为requestAnimationFrame只会在页面的下一次渲染前执行,页面渲染频率和屏幕的刷新率(60Hz, 120Hz, 144Hz)有关,可以保持在一定的频率下执行逻辑,可以参考