使用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

相關文章
相關標籤/搜索