chevron-left chevron-right

Piotr Nalepa Frontend, architecture and AI

[JS] Leniwa inicjalizacja danych w module za pomocą JS Promises

This version
blog.piotrnalepa.pl/2014/04/17/js-leniwa-inicjalizacja-danych-w-module-za-pomoca-js-promises
Published
Author
, Staff Software Engineer
Category
JavaScript
Feedback
Write to me · Post on X

This post is 12 years old. Browsers and tools may have changed since then, so check the code against current documentation before you use it.

Jakiś czas temu pisałem o sposobach wykorzystania JS Promises w codziennej pracy jako webdeveloper. Celem tego artykułu jest przedstawienie praktycznego sposobu na użycie tej funkcjonalności JS.

Section 1Opis przypadku użycia

W przypadku tego wpisu zakładam, że posiadasz wystarczającą wiedzę odnośnie JS i masz przeczytany mój poprzedni wpis odnośnie JS Promises.

Wyobraźmy sobie sytuację, że chcemy utworzyć listę użytkowników na stronie. Każdy użytkownik jest osobnym obiektem na którym możemy wykonywać różnorodne działania. W momencie inicjalizacji obiektu użytkownika nie posiadamy żadnych danych o użytkowniku i zamierzamy je dopiero pobrać z zewnętrznego źródła, aby potem je użyć do wyświetlenia informacji na stronie. To wszystko brzmi prosto i fajnie, ale nie jest to trywialne zagadnienie.

Section 2Rozwiązanie problemu - podejście 1

Na początek przedstawię problem jaki może zaistnieć w takiej sytuacji, gdy będziemy chcieli od razu wyświetlić dane użytkownika na stronie.

Załóżmy, że mamy przygotowany kod HTML, który będzie wyglądał następująco:

Example 1HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- index.html -->
<div class="task api-1"></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script id="user-profile" type="text/template">
<div class="user">
  <img src="<%= picture %>" alt="">
  <div class="info">
    <h3><%= name.first%> <%= name.last%></h3>
    <dl>
      <dt>Address:</dt>
      <dd><%= location.street%><br/><%= location.zip%> <%= location.city%>, <%= location.state%></dd>
      <dt>Phone:</dt>
      <dd><%= cell%></dd>
      <dt>Email:</dt>
      <dd><%= email%></dd>
    </dl>
  </div>
</div>
</script>
<script src="module-user-promise.js"></script>

Do wyświetlenia danych o jednym użytkowniku skorzystamy z funkcjonalności szablonów z biblioteki lodash.js. Jak można zauważyć, na stronie będą mogły zostać wyświetlone: zdjęcie profilu, nazwa użytkownika i jego dane.

Teraz pora na kod JS, który większość z programistów w takiej sytuacji by napisała:

Example 2JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// module-user-promise.js
API = window.API || {};
 
API.User = function User(gender) {
  var userObject    = {};
  var createProfile = function () {
    $('.api-1').append(_.template($('#user-profile').html(), userObject));
  };
 
  $.getJSON('http://api.randomuser.me/?gender=' + gender, function (result) {
    userObject = result.results[0].user;
  });
 
  return {
    getName : function () {
      console.log(userObject.name.first + ' ' + userObject.name.last);
    },
    getAddress : function () {
      console.log(userObject.location);
    },
    updateAddress : function (data) {
      _.assign(userObject.location, data);
 
      if ($('.user').length) {
        $('.user').remove();
        createProfile();
      }
    },
    renderProfile : function () {
      createProfile();
    }
  };
};
 
var jan = new API.User('male');
 
jan.renderProfile();

To jest niepoprawne rozwiązanie problemu. Na pierwszy rzut oka to wszystko wygląda ok, ale mamy tutaj do czynienia z problemem danych, których nie ma w momencie inicjalizacji obiektu. Jak można zauważyć, wysyłamy zapytanie do serwisu z danymi (w tym przypadku będzie to serwis: Random User Generator) i zakładamy że te dane od razu będą dostępne w momencie wywołania metod obiektu. Niestety, ze względu na asynchroniczność kodu, dane są dostarczane po inicjalizacji obiektu co powoduje problemy i kod reaguje tak, jakby tych danych nie było.

Section 3Rozwiązanie problemu - podejście 2, właściwe

Na szczęście, rozwiązanie jest proste i opiera się na obiecankach JS. Przy tworzeniu tego typu rozwiązań trzeba założyć, że danych nie ma w momencie inicjalizacji obiektu (ten przypadek rozważamy) i może ich w ogóle nie być (tego przypadku nie będziemy rozważać w tym artykule).

Kod, który rozwiąże ten problem wygląda następująco:

Example 3JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// module-user-promise.js
API = window.API || {};
 
API.User = function User(gender) {
  var userObject    = {};
  var defer         = $.Deferred();
  var createProfile = function () {
    $('.api-1').append(_.template($('#user-profile').html(), userObject));
  };
 
  $.getJSON('http://api.randomuser.me/?gender=' + gender, defer.resolve);
 
  defer.then(function (result) {
    userObject = result.results[0].user;
  });
 
  return {
    getName : function () {
      defer.done(function () {
        console.log(userObject.name.first + ' ' + userObject.name.last);
      });
    },
    getAddress : function () {
      defer.done(function () {
        console.log(userObject.location);
      });
    },
    updateAddress : function (data) {
      defer.done(function () {
        _.assign(userObject.location, data);
 
        if ($('.user').length) {
          $('.user').remove();
          createProfile();
        }
      });
    },
    renderProfile : function () {
      defer.done(function () {
        createProfile();
      });
    }
  };
};
 
var jan = new API.User('male');
 
jan.renderProfile();

W powyższym kodzie użyto funkcjonalności Deferred, czyli implementacji JS Promises wg pomysłu twórców biblioteki jQuery. Co ważne, na początku stworzyliśmy obiekt defer, który będzie odpowiedzialny za reagowanie na działanie kodu. To oznacza, że w momencie wysyłania zapytania do serwisu za pomocą $.getJSON() jako callback (funkcję sukcesu) ustawiamy defer.resolve, który będzie przechowywał informacje o pomyślnym przebiegu realizacji zapytania. Następnie, za pomocą defer.then(), określamy co się wydarzy w momencie otrzymania odpowiedzi z serwisu, czyli przekazujemy dane do zmiennej prywatnej obiektu User.
W metodach publicznych obiektu User korzystamy z defer.done(). Może Ciebie dziwić dlaczego korzystam z tego w każdej funkcji osobno, ale odpowiedź jest prosta. Jako twórca tego swoistego API nie mogę być pewny, kiedy dostanę odpowiedź z serwisu i czy dane będą dostępne w momencie wywołania metody.

Section 4Podsumowanie

Mam nadzieję, że powyższa propozycja rozwiązania problemu z dostepnością danych w momencie inicjalizacji obiektu będzie pomocna dla Ciebie. Uznałem, że warto się podzielić tym rozwiązaniem, bo dzięki niemu mamy o wiele lepiej rozwiązany problem związany użytkowaniem API.

Zapraszam do komentowania kodu i dzielenia się swoimi spostrzeżeniami tutaj, za pomocą komentarzy, lub na Githubie

Share
Feedback

About the author

Staff Software Engineer

I've been writing this blog since 2009 and building for the web professionally for 14 years. Today I lead the frontend of the Cosmos platform at Bishop Fox, where I've been since day one of the project. Before that I worked at OLX, KUDO, Auto1 and eZ Systems. Here I write about what I've learned in practice: JavaScript, TypeScript, React, CSS and frontend architecture. I've also spoken at conferences across Poland and Europe. Got a question or an idea for a post? Let me know!