將頁面分為時間顯示部分,控制部分,顯示計次共三個部分。實現的功能有:啟動定時器,計次,停止,復位。
計算:當前顯示的時間 = 當前計次的累積時間 + 已經結束的所有計次的累積時間和;
關於 new Date().getTime() 實現,google准確,Firefox 誤差很大;
涉及到的時間計算,都是用 setInterval實現,沒有用 new Date();
嘗試過setInterval 與 new Date兩者混用,一是誤差很大,二是邏輯不夠強;
經測試在google瀏覽器和IOS原組件的誤差很小(毫秒級別),准確度可靠;Firefox 誤差很大;
1class Stopwatch {
2 constructor(id) {
3 this.container = document.getElementById(id);
4 this.display = this.container.querySelector('.display'); // 時間顯示
5 this.lap = this.container.querySelector('.lap'); // 計次顯示
6
7 // 計數相關變量
8 this._stopwathchTimer = null; // 計時器
9 this._count = 0; // 計次的次數
10 this._timeAccumulation = 0; // 累積時長
11 this._timeAccumulationContainer = []; // 存放已經結束的計次的容器
12 this._s = 0; // 已經結束的所有計次累積時間
13 this._stopwatchHandlers = []; // 用於tartTimer里回調的函數
14
15 // 控制流
16 this.ctrl = this.container.querySelector('.ctrl'); // 控制部分
17 if(this.ctrl) {
18 let btns = this.ctrl.querySelectorAll('button');
19 let startStopBtn = btns[1]; // 開始和暫停按鈕
20 let lapResetBtn = btns[0]; // 計次和復位按鈕
21
22 // 樣式更改
23 let changeStyle = {
24 clickStart : function(){
25 lapResetBtn.disabled = ''; // 計次按鈕生效
26 startStopBtn.innerHTML = '停止';
27 startStopBtn.className = 'stop';
28 lapResetBtn.innerHTML = '計次';
29 lapResetBtn.className = 'active';
30 },
31 clickStop : function() {
32 startStopBtn.innerHTML = '啟動';
33 startStopBtn.className = 'start';
34 lapResetBtn.innerHTML = '復位';
35 },
36 clickReset : function() {
37 lapResetBtn.disabled = 'disabled'; // 計次按鈕失效
38 lapResetBtn.innerHTML = '計次';
39 lapResetBtn.className = '';
40 this.display.innerHTML = '00:00.00';
41 this.lap.innerHTML = '';
42 }
43 };
44
45 // 事件處理函數
46 let eventHandler = {
47 start: function() {
48 lapResetBtn.removeEventListener('click', resetBind); // 移除復位事件;選擇啟動,就移除復位
49 console.log('啟動');
50 changeStyle.clickStart.call(this); // 改變按鈕顯示樣式
51 if(this._count === 0) { // 如果首次啟動計時器,增加一條計次
52 this._count = 1;
53 // console.log('開始事件中的計數次', this._count)
54 this.insertLap(); // 插入計次
55 }
56 this.startTimer();
57 startStopBtn.removeEventListener ('click', startBind); // 移除啟動計時事件
58 lapResetBtn.addEventListener('click', lapfBind) // 添加計次事件
59 startStopBtn.addEventListener('click', stopBind) // 添加停止計時事件
60 },
61
62 stop: function() {
63 console.log('停止');
64 changeStyle.clickStop.call(this); // 改變按鈕顯示樣式
65 this.stopTimer(); // 停止計時;
66 startStopBtn.removeEventListener('click', stopBind) // 移除停止計時事件
67 startStopBtn.addEventListener('click', startBind); // 重新添加啟動計時事件
68 lapResetBtn.removeEventListener('click', lapfBind); // 移除計次事件;
69 lapResetBtn.addEventListener('click', resetBind); // 添加復位事件
70 },
71
72 lapf: function() {
73 this.insertLap(); // 插入新計次
74 this._timeAccumulationContainer.push(this._timeAccumulation); // 將當前結束的計次推入容器,保存起來
75 this._s += this._timeAccumulationContainer[this._count - 1]; // 累加已經結束的所有計次
76 console.log('計次', '當前累積的計次時間', this._s);
77 this._timeAccumulation = 0; // 計時器清零,這條放在求和后面!
78 this._count++;
79 },
80
81 reset: function() { // 復位事件
82 console.log('復位');
83 changeStyle.clickReset.call(this); // 改變按鈕顯示
84 // 重置
85 this._stopwathchTimer = null;
86 this._count = 0;
87 this._timeAccumulation = 0;
88 this._timeAccumulationContainer = [];
89 this._s = 0;
90 lapResetBtn.removeEventListener('click', resetBind); // 復位是所有事件中最后綁定的用完應該刪除
91 }
92 }
93
94 // 事件綁定
95 // 事件函數副本
96 let startBind = eventHandler.start.bind(this), // bind 每次會弄出新函數...
97 stopBind = eventHandler.stop.bind(this),
98 lapfBind = eventHandler.lapf.bind(this),
99 resetBind = eventHandler.reset.bind(this);
100 startStopBtn.addEventListener('click', startBind);
101 }
102
103 // 用於監聽startTimer
104 this.addStopwatchListener(_timeAccumulation => {
105 this.displayTotalTime(_timeAccumulation);
106 })
107 this.addStopwatchListener(_timeAccumulation => {
108 this.displayLapTime(_timeAccumulation);
109 })
110 }
111
112 // API
113 // 計時器
114 startTimer() {
115 this.stopTimer();
116 this._stopwathchTimer = setInterval(() => {
117 this._timeAccumulation++; // 注意時間累積量 _timeAccumulation 是厘秒級別的(因為界面顯示的是兩位)
118 this._stopwatchHandlers.forEach(handler => { // 處理回調函數
119 handler(this._timeAccumulation);
120 })
121 }, 1000 / 100)
122 }
123
124 stopTimer() {
125 clearInterval(this._stopwathchTimer );
126 }
127
128 // 總時間顯示(從啟動到當前時刻的累積時間)
129 displayTotalTime(_timeAccumulation) {
130 let totaltimeAccumulation = this._timeAccumulation * 10 + this._s * 10; // _s為_timeAccumulation累積時間隊列之和;
131 this.display.innerHTML = `${this.milSecond_to_time(totaltimeAccumulation)}`;
132 }
133 // 計次條目顯示
134 displayLapTime(_timeAccumulation) {
135 let li = this.lap.querySelector('li'),
136 spans = li.querySelectorAll('span'),
137 task = spans[0], time = spans[1];
138
139 task.innerHTML = `計次${this._count}`;
140 time.innerHTML = `${this.milSecond_to_time(this._timeAccumulation * 10)}`;
141 }
142
143 // 插入一個計次
144 insertLap() {
145 let t = this.templateLap(); // 顯示計次
146 this.lap.insertAdjacentHTML('afterBegin', t);
147 }
148 // 計次內容模板
149 templateLap() {
150 let t = `
151 <li><span></span><span></span></li>
152 `
153 return t;
154 }
155
156 // 將時間累積量轉化成時間
157 milSecond_to_time(t) { // t 時間間隔,單位 ms
158 let time,
159 minute = this.addZero(Math.floor(t / 60000) % 60), // 分
160 second = this.addZero(Math.floor(t / 1000) % 60), // 秒
161 centisecond = this.addZero(Math.floor(t / 10) % 100) ; // 厘秒(百分之一秒)
162 time = `${minute}:${second}.${centisecond}`;
163 return time;
164 }
165 // 修飾器;加零
166 addZero(t) {
167 t = t < 10 ? '0' + t : t;
168 return t;
169 }
170 // 添加監聽startTimer的事件函數
171 addStopwatchListener(handler) {
172 this._stopwatchHandlers.push(handler);
173 }
174}
175
176// 調用
177const stopwatch = new Stopwatch('stopwatch');
一個200行的小demo,收獲不少
從基於實現組件功能開始,到使用class封裝組件;
最小化訪問DOM元素;
相關變量放在一起,將樣式更改函數放在一塊,將事件處理函數放在一塊;
綁定this(非箭頭函數this丟失),bind的時候每次都會重新生成新函數(將函數bind后統一賦給一個變量,這樣增加事件和刪除事件所用的函數就是同一個了);
增加事件監聽器,統一管理需要調用函數變量的系列相關事件;
將函數抽象到最純(函數就是函數不與組件的元素相互耦合),使用Decorate(裝飾器);
由於在同一個按鈕上綁定了不同的事件,因此事件綁定與移除的順序很重要;
https://rencoo.github.io/appDemo/iosStopwatch/index.html
另外一篇文章, 用狀態模式重構了這個小demo https://www.cnblogs.com/rencoo/p/10115341.html