JavaScript - odroczenie wywołania funkcji szybsze niż setTimeout, to możliwe?
ProgramowanieDoś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.