JavaScript Хронометър — част 2

По пътя към JS Хронометър сме направили:

  • една семпла страница с основен HTML markup
  • приложили сме нужните стилове
  • заредили сме jQuery и сме задали функция, която да се изпълни при DOMContentLoaded

Остава ни единствено да добавим и програмната логика на нашия хронометър. Тогава той ще може да ни свърши някаква работа.

Нека променим текста на хронометъра от 00:00:00 на 00:00:00,000. Просто добавяме и милисекунди, за да наблюдаваме по-детайлно промените във времето на хронометъра.

Аз самият предпочитач първо да си създам няколко променливи, които ще използвам в кода си, след което вече променям стойността им:

jQuery(function ($) {
	var trigger = $('#stopwatch a');
	var label = $('#stopwatch span');
});

В тези две променливи ще запишем референция към елементите на страницата, които ще използваме. Аз лично предпочитам да ги запиша в отделни променливи поради две причини:

  1. по-нататъшното използване е по-лесно, отколкото отново да пишем селектора
  2. селектираме 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/

Вашият коментар

Вашият имейл адрес няма да бъде публикуван. Задължителните полета са отбелязани с *

Този сайт използва Akismet за намаляване на спама. Научете как се обработват данните ви за коментари.