使用TypeScript实现一个Ticker

背景

一般在开发一个项目的时候,总会有很多场景须要建立定时器,这会致使项目中出现不少重复的代码。为了解决这个问题,不妨构建一个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

相关文章
相关标签/搜索