Przejdź do treści

Popularny błąd w obsłudze zdarzeń w JavaScript

Chciałbym dzisiaj przedstawić problem z jakim możecie się spotkać podczas obsługi zdarzeń. Naturą języka jest asynchroniczność, spotykana jest w wielu miejscach. Asynchroniczne zapytania do serwera, czyli AJAX, to technika bardzo popularna w dzisiejszych czasach.

Pewnym rodzajem asynchroniczności są też zdarzenia aplikacji. Częsty problem początkujących developerów objawia się tym, że próbują oni dostać się do DOMu, kiedy nie jest on jeszcze załadowany. Trzeba wtedy skorzystać z window.onload (albo lepszej formy window.addEventListener('DOMContentLoaded', ...').
Ewentualnie można skorzystać z load (window.addEventListener('load', ...) jeśli event DOMContentLoaded nie jest wspierany.

Baner promujący artykuł

Customowe eventy

Customowe eventy, to bardzo popularny wzorzec w aplikacjach webowych. Zdarzenia te nie są wyzwalane przez interakcję użytkownika, tylko przez samą aplikację. Dobrym przykładem może być zapytanie do serwera.

Zerknijmy na taki kod:

function ajax(url) {
    var req = new Request(url);
    req.on('load', function () {
        Request.emit('success');
    });
    req.on('error', function () {
        Request.emit('error');
    });
}

Mamy zapytanie do serwera, gdzie na zakończenie wyzwolone zostanie odpowiednie zdarzenie. Kiedy zapytanie się powiedzie, zostanie wyzwolone zdarzenie success, w przypadku błędu - error.

Chcielibyśmy teraz skorzystać z funkcji ajax, aby pobrać dane dotyczące użytkownika. Wykonujemy więc taki kod:

Request.once('success', (response) => {
    loadUserProfile(response);
});
Request.once('error', () => {
    loadFailUserPage();
});
ajax('/user');

Nasłuchujemy na 2 zdarzenia:

  • na poprawne pobranie danych z serwera wyświetlamy profil użytkownika
  • kiedy zapytania zakończy się niepowodzeniem wyświetlamy błąd załadowania profilu

Na pierwszy rzuta oka nie ma tutaj nic dziwnego. A jednak. Co jeśli skorzytamy z funkcji ajax w innym miejscu? Chcemy pobrać zawartość kategorii produktów w sklepie internetowym, korzystając oczywiście z funkcji ajax.

Request.once('success', (response) => {
    loadCategory(response);
});
Request.once('error', () => {
    loadErrorCategoryPage();
});
ajax('/category/1234');

Mamy problem. Dlaczego? Bo nie wyrejestrowaliśmy handlerów z pierwszego zapytania. Powiecie: "No ale przecież używamy tam once, to handler sam się wyrejestruje po wykonaniu zdarzenia." Zgadza się, ale tylko użyty handler się wyrejestruje. Nigdy nie będzie sytuacji, że zapytanie uruchomi dwa zdarzenie success i error.

Diagram z opisywaną sytuacją. Zakreśliłem problem jaki się pojawił owalem.

Rozwiązanie problemu

Jest kilka rozwiązań opisywanego przeze mnie problemu. Najlepsze (dla użytego kodu) jest takie, aby w dowolnym handlerze wyrejestrować oba handlery.

function categoryHelperSuccess(response) {
    loadCategory(response);
    clearCategoryHelpers(); // Usuwamy niepotrzebne handler
};

function categoryHelperError() {
    loadErrorCategoryPage();
    clearCategoryHelpers();
};

// Usuwamy nasłuchiwanie na zdarzenia: success i error,
// aby zapobiec problemowi uruchamiania niepotrzebnych handlerów.
function clearCategoryHelpers() {
    Request.off('success', categoryHelperSuccess);
    Request.off('error', categoryHelperError);
};

Request.once('success', categoryHelperSuccess);
Request.once('error', categoryHelperError);

ajax('/category/1234');

Najlepsze rozwiązanie leży u podstaw tego kodu. Nie należy zapisywać się na zdarzenia obiektu współdzielonego. Wtedy nie będzie problemu. Lepsze rozwiązania w tym przypadku to używanie Promise-ów albo callback-ów.

Testowy projekt

Stworzyłem na potrzeby tego artykułu projekt na GitHubie. Chciałem pokazać Wam jak na faktycznym kodzie, objawia się opisywany przeze mnie problem.

Wystarczy uruchomić przykład w przeglądarce (plik demo/index.html), albo w terminalu uruchomić polecenie node index.js, aby zobaczyć opisywany problem. Poniżej zrzut ekranu z błędem.

Błąd, który pojawi się w konsoli po stworzenie opisywanej architektury.