js倒计时

摘抄自:https://zhuanlan.zhihu.com/p/20832837

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* @author xiaojue
* @date 20160420
* @fileoverview 倒计时想太多版
*/
(function() {

function timer(delay) {
this._queue = [];
this.stop = false;
this._createTimer(delay);
}

timer.prototype = {
constructor: timer,
_createTimer: function(delay) {
var self = this;
var first = true;
(function() {
var s = new Date();
for (var i = 0; i < self._queue.length; i++) {
self._queue[i]();
}
if (!self.stop) {
var cost = new Date() - s;
delay = first ? delay : ((cost > delay) ? cost - delay : delay);
setTimeout(arguments.callee, delay);
}
})();
first = false;
},
add: function(cb) {
this._queue.push(cb);
this.stop = false;
return this._queue.length - 1;
},
remove: function(index) {
this._queue.splice(index, 1);
if(!this._queue.length){
this.stop = true;
}
}
};

function TimePool(){
this._pool = {};
}

TimePool.prototype = {
constructor:TimePool,
getTimer:function(delayTime){
var t = this._pool[delayTime];
return t ? t : (this._pool[delayTime] = new timer(delayTime));
},
removeTimer:function(delayTime){
if(this._pool[delayTime]){
delete this._pool[delayTime];
}
}
};

var delayTime = 1000;
var msInterval = new TimePool().getTimer(delayTime);

function countDown(config) {
var defaultOptions = {
fixNow: 3 * 1000,
fixNowDate: false,
now: new Date().valueOf(),
template: '{d}:{h}:{m}:{s}',
render: function(outstring) {
console.log(outstring);
},
end: function() {
console.log('the end!');
},
endTime: new Date().valueOf() + 5 * 1000 * 60
};
for (var i in defaultOptions) {
if (defaultOptions.hasOwnProperty(i)) {
this[i] = config[i] || defaultOptions[i];
}
}
this.init();
}

countDown.prototype = {
constructor: countDown,
init: function() {
var self = this;
if (this.fixNowDate) {
var fix = new timer(this.fixNow);
fix.add(function() {
self.getNowTime(function(now) {
self.now = now;
});
});
}
var index = msInterval.add(function() {
self.now += delayTime;
if (self.now >= self.endTime) {
msInterval.remove(index);
self.end();
} else {
self.render(self.getOutString());
}
});
},
getBetween: function() {
return _formatTime(this.endTime - this.now);
},
getOutString: function() {
var between = this.getBetween();
return this.template.replace(/{(\w*)}/g, function(m, key) {
return between.hasOwnProperty(key) ? between[key] : "";
});
},
getNowTime: function(cb) {
var xhr = new XMLHttpRequest();
xhr.open('get', '/', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 3) {
var now = xhr.getResponseHeader('Date');
cb(new Date(now).valueOf());
xhr.abort();
}
};
xhr.send(null);
}
};

function _cover(num) {
var n = parseInt(num, 10);
return n < 10 ? '0' + n : n;
}

function _formatTime(ms) {
var s = ms / 1000,
m = s / 60;
return {
d: _cover(m / 60 / 24),
h: _cover(m / 60 % 24),
m: _cover(m % 60),
s: _cover(s % 60)
};
}

var now = Date.now();

new countDown({});
new countDown({
endTime: now + 8 * 1000
});

})();

主要说思路,实现分成几个步骤,第一个类是timer,通过setTimeout创建一个倒计时实例,内部对阻塞误差做了矫正,但是缺少一步,如果阻塞大于延迟时间,那么应该下次递归直接执行,而我这里还是会延迟1s,只是相对会减少误差。然后这个timer通过add和remove方法管理一个回调队列,让所有通过这个timer实现的倒计时都共用一个定时器。第二个类TimePool,主要实现一个timer池子,我们之前只想到了倒计时都是1s的,那如果有500ms或者10s的倒计时,这里这个池子是可以装多个timer类的,保证不同延迟下,定时器最小。最后一个类就是countDown类,里面同样是默认配置起手,然后主要介绍fixNowDate这个参数,是用来进行系统时间修正的,这里默认的实现是浏览器端的一个修正技巧,主要参见getNowTime方法,通过一个xhr请求拿服务端headers中的Date来进行矫正。

之后摘出来了一个template,对输出的时间格式可以做一个简单的模板替换,抽象了3个公共类,最后的countDonw依赖其他2个类做了最终实现。最后,这段代码也可以复用在nodejs端,只需要修改getNowTime的方法为nodejs的即可。

我的这段实现,没有考虑太多样式的可扩展,更多是性能上的,但是真实情况下性能如何,还需要实际验证,这一步是没有做的,而且只适用页面多定时器和倒计时的场景,可能还存在一些隐藏的未知bug