一般在開發一個項目的時候,總會有很多場景須要建立定時器,這會致使項目中出現不少重複的代碼。爲了解決這個問題,不妨構建一個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。