一般在开发一个项目的时候,总会有很多场景须要建立定时器,这会致使项目中出现不少重复的代码。为了解决这个问题,不妨构建一个Ticker来维护整个项目的时间线。git
先用一个思惟导图来理清思路:github
全局使用设计模式
当Ticker须要做用在整个项目中时,最好的设计模式就是单例。为了使用方便,结合静态方法构造Ticker的雏形。数组
export class Ticker {
static _ticker: Ticker = new Ticker();
_sayHello () {
console.log('Hello.');
}
static sayHello () {
this._ticker._sayHello();
}
}
复制代码
方法友好便于维护浏览器
采用单例模式结合静态方法,就能够达到经过类名.方法
的方式直接调用实例中的方法,例如执行Ticker.sayHello()
,会获得控制台打印Hello.
的结果。服务器
定时器的隐式问题框架
相信不少人都对setTimeout()
和setInterval()
很是熟悉,但不是全部人都会它们的运行机制有了解。异步
定时器会把方法放入异步队列,哪怕第二个参数设置为0。异步队列中的方法会在同步队列执行完毕以后才会执行。函数
这里简单介绍一下setTimeout()
与setInterval()
。setTimeout()
入参中的delay是仅仅是等待时间,而setInterval()
入参中的delay还包括了执行时间,也就是说一样设置delay,setTimeout()
间隔会略长于setInterval()
。同时,现代浏览器对setInterval()
有一个优化,就是当主线程阻塞时,浏览器只会保持setInterval()
回调方法队列中仅存在一个待执行方法,而不会像以前出现连续执行若干次回调方法的状况。工具
另外,考虑到上述setInterval()
特性,在主线程阻塞的状况下会有机会出现两次回调函数结束时间小于delay的状况,为了不这种状况可能致使的问题,采用链式setTimeout调用来维护时间线。
以前对这里描述比较模糊,感谢碎碎酱提醒。
简单粗暴一点,直接上代码,并在代码中逐步解释做用。
/* * @Author: 伊丽莎不白 * @Date: 2019-07-05 17:17:30 * @Last Modified by: 伊丽莎不白 * @Last Modified time: 2019-07-10 15:01:30 */
export class Ticker {
static _ticker: Ticker = new Ticker();
_running: boolean = false; // 正在运行
_systemTime: number = 0; // 系统时间
_lastTime: number = 0; // 上次执行时间
_timerId: NodeJS.Timeout = null; // 计时器id
_delay: number = 33; // 延时设定
_funcs: Array<Function> = []; // 钩子函数队列
_executeFuncs: Array<ExecuteValue> = []; // 定时执行函数队列,按执行时间升序排序
constructor () {
}
/** * 查找第一个大于目标值的值的下标 * @param time */
_searchIndex (time: number) {
let funcs: Array<ExecuteValue> = this._executeFuncs;
let low: number = 0;
let high: number = funcs.length;
let mid: number = 0;
while (low < high) {
mid = Math.floor(low + (high - low) / 2);
if (time >= funcs[mid].time) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return low;
}
/** * 注册钩子函数 * @param func 执行函数 */
_register (func: Function) {
if (this._funcs.includes(func)) {
return;
}
this._funcs.push(func);
}
/** * 注册一个函数,在一段时间以后执行 * @param func 执行函数 * @param delay 延时 * @param time 执行时系统时间 * @param loop 循环次数 */
_registerDelay (func: Function, delay: number, time: number, loop: number) {
// 先查找后插入
let index: number = this._searchIndex(time);
let value: ExecuteValue = { func: func, time: time, delay: delay, loop: loop };
this._executeFuncs.splice(index, 0, value);
}
/** * 注册一个函数,在某个时间点执行 * @param func 执行函数 * @param time 执行时间 */
_registerTimer (func: Function, time: number) {
// 先查找后插入
let index: number = this._searchIndex(time);
let value: ExecuteValue = { func: func, time: time };
this._executeFuncs.splice(index, 0, value);
}
/** * 移除钩子函数 * @param func 执行函数 */
_unregister (func: Function) {
this._funcs.map((value: Function, index: number) => {
if (func === value) {
this._funcs.splice(index, 1);
}
});
}
/** * 启动Ticker,并设置当前系统时间,一般与服务器时间同步 * @param systemTime 系统时间 */
_start (systemTime: number = 0) {
if (this._running) {
return;
}
this._running = true;
this._systemTime = systemTime;
this._lastTime = new Date().getTime();
this._update();
}
/** * 链式执行定时器,钩子函数队列为每次调用必执行,定时执行函数队列为系统时间大于执行时间时调用并移出队列 */
_update () {
let currentTime: number = new Date().getTime();
let delay: number = currentTime - this._lastTime;
this._systemTime += delay;
// 钩子函数队列,依次执行便可
this._funcs.forEach((value: Function) => {
value(delay);
});
this._executeFunc();
this._lastTime = currentTime;
this._timerId = setTimeout(this._update.bind(this), this._delay);
}
/** * 执行定时函数 */
_executeFunc () {
// 取数组首项进行时间校验
if (this._executeFuncs[0] && this._executeFuncs[0].time < this._systemTime) {
// 取出数组首项并执行
let value: ExecuteValue = this._executeFuncs.shift();
value.func();
// 递归执行下一项
this._executeFunc();
// 判断重复执行次数
if (value.hasOwnProperty('loop')) {
if (value.loop > 0 && --value.loop === 0) {
return;
}
// 计算下次执行时间,插入队列
let fixTime: number = value.time + value.delay;
this._registerDelay(value.func, value.delay, fixTime, value.loop);
}
}
}
/** * 中止Ticker */
_stop () {
if (this._timerId) {
clearTimeout(this._timerId);
this._timerId = null;
}
this._running = false;
}
/** * 公开的钩子函数注册方法 * @param func 执行函数 */
static register (func: Function) {
this._ticker._register(func);
}
/** * 公开的钩子函数移除方法 * @param func 执行函数 */
static unregister (func: Function) {
this._ticker._unregister(func);
}
/** * 公开的延时执行函数方法,用户可设置执行次数,loop为0时无限循环 * @param func 执行函数 * @param delay 延时 * @param loop 循环次数 */
static registerDelay (func: Function, delay: number, loop: number = 1) {
let time: number = this._ticker._systemTime + delay;
this._ticker._registerDelay(func, delay, time, loop);
}
/** * 公开的定时执行函数方法 * @param func 执行函数 * @param time 执行时间 */
static registerTimer (func: Function, time: number) {
this._ticker._registerTimer(func, time);
}
/** * 公开的启动方法 * @param systemTime 系统时间 */
static start (systemTime: number = 0) {
this._ticker._start(systemTime);
}
/** * 公开的中止方法 */
static stop () {
this._ticker._stop();
}
/** * 系统时间 */
static get systemTime (): number {
return this._ticker._systemTime;
}
/** * 正在运行 */
static get running (): boolean {
return this._ticker._running;
}
}
interface ExecuteValue {
func: Function;
time: number;
delay?: number;
loop?: number;
}
复制代码
如何使用
建议在项目启动的时候执行Ticker.start()
方法,此时Ticker中的systemTime将从0开始计时;或者在获取到服务器时间以后执行并传入服务器时间Ticker.start(serverTime)
,这样项目将会在项目中维持服务器时间线。若是是须要时间校验的业务,能够考虑第二种方法。
对于Ticker的扩展,往大说或许能够说不少,甚至发展为一个时间线框架。但我目前也只是将它用做平时项目里的一个趁手的工具。若是有兴趣,能够与我讨论,也能够随意增添一些个性化的功能。
完整的代码与使用用法,请移步GitHub。