
JavaScript Хронометър — част 2
По пътя към JS Хронометър сме направили:
- една семпла страница с основен HTML markup
- приложили сме нужните стилове
- заредили сме jQuery и сме задали функция, която да се изпълни при DOMContentLoaded
Остава ни единствено да добавим и програмната логика на нашия хронометър. Тогава той ще може да ни свърши някаква работа.
Нека променим текста на хронометъра от 00:00:00 на 00:00:00,000. Просто добавяме и милисекунди, за да наблюдаваме по-детайлно промените във времето на хронометъра.
Аз самият предпочитач първо да си създам няколко променливи, които ще използвам в кода си, след което вече променям стойността им:
jQuery(function ($) {
var trigger = $('#stopwatch a');
var label = $('#stopwatch span');
});
В тези две променливи ще запишем референция към елементите на страницата, които ще използваме. Аз лично предпочитам да ги запиша в отделни променливи поради две причини:
- по-нататъшното използване е по-лесно, отколкото отново да пишем селектора
- селектираме DOM елемент само веднъж, защото това действие по принцип е бавно, така че е добре да ограничим селектирането на отделни възли от markup-а си
Обработване на click
събитие
След това нека да зададем click handler на нашия линк:
jQuery(function ($) {
var trigger = $('#stopwatch a');
var label = $('#stopwatch span');
trigger.click(function (e) {
e.preventDefault();
});
});
Извикваме e.preventDefault()
за да ограничим действието по подразбиране при кликването на линк, което ще се опита да ни прати на друга страница (в случая href
атрибутат е просто “#”, което всъщност няма да ни прати никъде, но все пак би ни върнало най-горе в страницата, което понякога може да е нежелан ефект).
Сега трябва да направим проверка в кода си, дали хронометъра вече не върви и в зависимост от това трябва да го пуснем или да го спрем. Самото пускане нека асоциираме с нова променлива, чиято стойност всъщност ще бъде и същинската проверка. В тази променлива ще запазим едно повтарящо се действие, което ще обновява показанието на хронометъра. Ако тази прометлива няма стойност — стартираме, в противен случай — спираме.
jQuery(function ($) {
var timer = null;
var startTime = 0;
var trigger = $('#stopwatch a');
var label = $('#stopwatch span');
trigger.click(function (e) {
e.preventDefault();
if (!timer) {
startTime = new Date();
} else {
}
});
});
setTimeout
и setInterval
Създаваме и променливата startTime
, която приравняваме първоначално на 0. При стартиране на хронометъра задаваме стойността ѝ да бъде моментът на стартирането. Сега вече следва да зададем и това повтарящо се действие, за което споменахме преди малко. Все още не сме говорили за подобни неща, но в JavaScript имаме възможност да извикаме дадена функция след определено време. За целта използваме setTimeout()
. Можем и да задаваме периодично изпълнение на функция, през даден интервал чрез setInterval()
. Самият синтаксис на функциите е еднакъв с разлика в това, че setTimeout()
изпълнява функцията само веднъж, а setInterval()
я изпълнява безкрайно. Или поне докато не я отменим чрез clearInterval()
.
jQuery(function ($) {
var FRAME_PERIOD = 1000/60;
var timer = null;
var startTime = 0;
var trigger = $('#stopwatch a');
var label = $('#stopwatch span');
trigger.click(function (e) {
e.preventDefault();
if (!timer) {
startTime = new Date();
timer = setInterval(function () {
var currentTime = new Date(new Date() - startTime);
label.text(formatTime(currentTime));
}, FRAME_PERIOD);
trigger.text('Stop');
} else {
clearInterval(timer);
timer = false;
trigger.text('Start');
}
});
});
Ето и коментарите към последния код:
В началото виждате добавената променлива FRAME_PERIOD
. Изписана е с главни букви, тъй като идеята ѝ е на константа. Както вече сме говорили — в JavaScript не можем да задаваме константи, затова използваме обикновена променлива. Стойността ѝ задаваме на 1000/60. От къде идва това число? Това е периодът на обновяване на картината на мониторите. Имаме 1000 милисекунди за секунда разделени на 60 Hz (обикновено поне е толкова) честота на опресняване на картината, отново за секунда.
В click handler-а на линка добавяме и код, който присвоява на променливата timer
периодично повтаряща се функция, зададена чрез setInterval
. Последната функция приема първи аргумент функция, която да изпълни, а като втори — период между изпълненията. Този период задаваме на времето на обновяване на картината на екрана. Тази функция ще изписва колко време е минало. За да изпишем нещо, трябва да имаме обновяване на картината. Иначе просто няма да можем да го прочетем.
Пресмятане на времето
В тялото на периодичната функция създаваме локална променлива, чиято стойност е дата. Тази дата е текущият момент минус моментът на натискане на бутона “Start”. Момент в JS се пази като брой милисекунди от 01.01.1970 00:00:00. Така че дата минус друга дата дава просто брой милисекунди между двата момента. Тази разлика третираме като нов момент, подавайки я като агрумент към new Date()
. След това на label
задаваме текст базиран на това време, но прекаран през функция, която сега ще опишем. Задачата на тази функция е да представи времето във хормат “00:00:00,000”.
function formatTime(time) {
return leadingZero(time.getUTCHours()) + ':' + leadingZero(time.getMinutes()) + ':' + leadingZero(time.getSeconds()) + ',' + leadingZero(time.getMilliseconds(), 3);
}
В тази функция използваме друга — leadingZero()
, която ще напишем след малко. Нейната задача ще е да представи едноцифрени числа с нули отпреде. Така че вместо 0:0:1,123 да имаме 00:00:01,123. Това, което правим във formatTime()
е да конкатенираме (свържем стрингове) от елементине на времето, които взимаме чрез няколко вградени функции за Date обектите. Тези функции са getUTCHours()
, getMinutes()
, getSeconds()
и getMilliseconds()
. Забележете, че използваме getUTCHours()
вместо обикновеното getHours()
, защото getHours()
връща часове според нашата часова зона. Изчислявайки разлика между моменти, ние получаваме разликата като период от времето по Гринуич. Така че в моя случай getHours()
би върнало 2, тъй като съм в GMT+2 часова зона (България).
А ето така изглежда и функцията leadingZero():
function leadingZero(num, length) {
if (typeof(length) == 'undefined') {
length = 2;
}
while (num.toString().length < length) {
num = '0' + num;
}
return num;
}
Първо забележете използването на втори аргумент. Извикване на функцията използвайки го имаме при форматирането на милисекундите, тъй като те не трябва да са само с 2 цифри, а с три. Именно броят на цифрите в числото упоменава чрез този втори аргумент.
Стойности по подразбиране на аргументи в JavaScript
По принцип функция може да се дефинира с незадължителни аргументи. Тогава се задава стойност по подразбиране. В повечето езици това би изглеждало като:
function leadingZero(num, length=2) {
// ...
}
В JavaScript това обаче не би имало никакъв ефект.
Нормален език би дал грешка, ако подадем 2 аргумента на функция, очакваща 3. JavaScript обаче просто гледа дали има съвпадение. Във функцията можем да се опитаме да достъпим различен брой аргументи. Ако са подадени — супер. Ако не са — ще получим стойност само на някои, а останалите ще имат стойност undefined
.
Затова правим проверката if (typeof(length) == 'undefined')
, което проверява дали променливана е недефинирана. Ако е, тогава трябва да използваме нейната стойност по подразбиране, затова ѝ присвояваме стойност 2.
След това с цикъл добавяме толкова нули пред числото, колкото е разликата в дължината на знаците, с които се изписва. Още след първото добавяне на 0, променливата вече не е от числов тип, а се третира като низов литерал (стринг). Защо и какво определя това ще обясня в друг урок, за момента просто е добре да го знаете на готово.
Ето го и пълния JavaScript:
jQuery(function ($) {
var FRAME_PERIOD = 1000/60;
var timer = null;
var startTime = 0;
var trigger = $('#stopwatch a');
var label = $('#stopwatch span');
function leadingZero(num, length) {
if (typeof(length) == 'undefined') {
length = 2;
}
while (num.toString().length < length) {
num = '0' + num;
}
return num;
}
function formatTime(time) {
return leadingZero(time.getUTCHours()) + ':' + leadingZero(time.getMinutes()) + ':' + leadingZero(time.getSeconds()) + ',' + leadingZero(time.getMilliseconds(), 3);
}
trigger.click(function (e) {
e.preventDefault();
if (!timer) {
startTime = new Date();
timer = setInterval(function () {
var currentTime = new Date(new Date() - startTime);
label.text(formatTime(currentTime));
}, FRAME_PERIOD);
trigger.text('Stop');
} else {
clearInterval(timer);
timer = false;
trigger.text('Start');
}
});
});
Demo на страницата пък можете да видите на https://magadanski.com/demo/stopwatch/