JavaScript - odroczenie wywołania funkcji szybsze niż setTimeout, to możliwe?

Programowanie

Doświadczeni programiści języka JavaScript doskonale znają magię funkcji window.setTimeout, która często jest niezastąpiona w rozwiązywaniu problemu błędów renderowania DOM, wyścigu wątków (pomijamy oczywiście celowe opóźnienie wywołania funkcji) itd. Tłumaczymy idee, sugerujemy dobrą praktykę i proponujemy szybszy zamiennik...


Doświadczeni programiści języka JavaScript doskonale znają magię funkcji window.setTimeout, która często jest niezastąpiona w rozwiązywaniu problemu błędów renderowania DOM, wyścigu wątków (pomijamy oczywiście celowe opóźnienie wywołania funkcji) itd. Najpopularniejszą i najprostszą składnią którą się spotyka jest:

setTimeout(function() {
[...]
}, 0);

Powyższy blok nie tyle ma na celu dosłowne "opóźnienie" wywołania funkcji (często właśnie tak - błędnie rozumiane przez początkujących programistów tego języka), a celem jest dodanie wywołania funkcji do kolejki wywołań w pętli zdarzeń. A więc, opóźnia jej wywołanie conajmniej o jeden przebieg w pętli zdarzeń - ile to czasu dokładnie zajmie nie wiemy, napewno wiemy że relatywnie mało. To z reguły wystarczy, aby pomóc w rozwiązaniu problemów powodowanych kolejką wywołań.

Bardziej zaawansowaną wersją robiącą dokładnie to samo jest następujący blok:

Function.prototype.async = function () {
    setTimeout.bind(null, this, 0).apply(null, arguments);
};

// using sample
alert.async('Jestem drugi!');
alert('Jestem pierwszy!');

Należy tutaj zauważyć że przybrana nazwa dla funkcji async jest błędna, ponieważ nie mamy tutaj działania asynchronicznego, choć może się nam tak wydawać. Kod ECMAScript jest wykonywany w wątku UI, który jest uruchomiony w danej (dowolnej) implementacji przeglądarki. Zasadniczo, wszystkie aktualizacje interfejsu użytkownika (a więc UI) są dzielone przez kod JavaScript w tym właśnie wątku (to jest również powód, dlaczego długo uruchomiony kod Javascript zawisa w każdej przeglądarce - wątek UI zostanie przeciążony zdaniami).

Działanie setTimeout należy odróżnić. Jego zadaniem jak wspomnieliśmy wyżej jest po prostu stanie w kolejce na wprowadzenie swojej funkcji do wątku UI. To zaś oznacza iż nigdy nie mamy gwarancji, że kod zostanie faktycznie wykonany w daną ilość milisekund i końcowo zostanie zepchnięty z kolejki. Jeśli inne procesy nadal działają lub przeglądarka jest zajęta w wątku UI, to funkcja nie zostanie wykonana dopóki coś jeszcze nie zostało przetworzone.

Dlatego też należy wiedzieć, że drugi argument będący ilością milisekund opóźnienia ustawiony na 0 - nigdy nim nie będzie. Ponadto każda przeglądarka ma określoną minimalną jej ilość, np. specyfikacja HTML5 mówi o wartości 4ms. Wracając do nazwy, powinna ona brzmieć raczej defer lub deffered - pod względem semantycznym bardziej odpowiada to rzeczywistości.

Często w praktyce użycie powyższych bloków powoduje - nadmierne opóźnienie wykonania kodu (a paradoksalnie rozwiązujemy inny problem generując kolejny). Chcemy jedynie przesunąć w kolejce wywołań nasze i najszybciej jak to możliwe go uruchomić. Na szczęście istnieje trzecia wersja rozwiązania problemu, co więcej bez użycia timeoutu. Da się? Oczywiście.

if (!Function.prototype.defer) {
	(function () {
		var timeouts = [],
			messageName = 'messageZeroTimeout';

		function setDeferTimeout(scope) {
			if (typeof this !== 'undefined') {
				timeouts.push(this.bind(scope));
			} else {
				timeouts.push(this);
			}
			window.postMessage(messageName, "*");
		}

		function handleMessage(event) {
			if (event.source !== window || event.data !== messageName) return false;
			event.stopPropagation();
			if (timeouts.length == 0) return false;
			var fn = timeouts.shift();
			fn();
		}

		window.addEventListener('message', handleMessage, true);

		Function.prototype.defer = setDeferTimeout;
	})();
}

Funkcja defer korzysta z mechanizmu window.postMessage zamiast setTimeout. Okazuje się że jest to rozwiązanie najszybsze i tak samo skuteczne jak klasyczny timeout. W porówaniu około 100 iteracji tej metody do klasycznego podejścia z setTimeout mamy 10-20ms / ok. 1s Funkcja ta często występuje na sieci jako setZeroTimeout w formie statycznej obiektu window, aczkolwiek jak wyżej wyjaśniliśmy nazwa zdecydowanie nie jest odpowiednia (podobnie jak async). Powyższa wersja jest to nasza modyfikacja wzbogacona dodatkowo o parametr scope.

Opublikowano Wrzesień 2015