ES6 – promises

javascript

Promises (obietnice?) to nie nowa idea. Każdego dnia używam ich w swoich projektach wraz z AngularJS $q, bazującym na bibliotece: kriskowal / q.

A tool for creating and composing asynchronous promises in JavaScript.

Jest to biblioteka do obsługi i radzenia sobie z asynchronicznością w JavaScript. Zanim jednak przejdę do obietnic, napiszę co nieco o tak zwanych callback‘ach oraz ich “piekle”.


Zobacz całą serię: Let-s talk about ECMAScript 2015


callback & callback hell

Odkąd pamiętam, deweloperzy JavaScript używają callbacków (funkcji zwrotnych) do obsługi asynchroniczności oraz wszelkich zapytań HTTP. Z czasem jednak może się okazać (i pewnie nie raz sami się na to nadzialiście), że powstają nam w kodzie takie oto potwory:

console.log('start!');
setTimeout(function () {
  console.log('ping');
  setTimeout(function () {
    console.log('pong');
    setTimeout(function () {
      console.log('end!');
    }, 1000);
  }, 1000);
}, 1000);
// start!
// after 1 sec: ping
// .. 1 sec later: pong
// .. and: end!

W tym prostym przykładzie użyłem funkcji setTimeout(), by pokazać wykonywanie funkcji callback, która odpalana jest z 1-sekundowym opóźnieniem. Wygląda to koszmarnie, czytelnością zupełnie nie zachwyca, a przecież to tylko trzy wywołania funkcji. Wyobraźcie sobie jak szybko w naszym kodzie może powstać takie spaghetti. Tworzenie tego typu piramid nazwane zostało terminem: callback hell.

Możecie o nim poczytać więcej na stronie: callbackhell.com

promises

By poradzić sobie z wyżej opisanym problemem – spotykanym praktycznie wszędzie – wymyślono obietnice. Nie lubię tego zwrotu, ale oddaje on wprost sens działania.

Promise to klasa, udostępniająca API reprezentujące wartość, która będzie dostępna w momencie, gdy wywołanie zostanie skończone, a sama wartość uzupełniona (spełnienie obietnicy).

Wraz z ECMAScript2015 otrzymujemy pełne, natywne wsparcie tej techniki.

Każdy promise może być w stanie:

  • fulfilled — pomyślnie obsłużony
  • rejected — odrzucony
  • pending —  w trakcie – ani fulfilled, ani rejected
  • settled — obsłużony, fulfilled lub rejected

Zwracany promise jest obiektem, który udostępnia nam metodę then(fn: onResolve, fn: onReject), pozwalającą obsłużyć promise w momencie, gdy przechodzi on do stanu settled.

Tak, każdy promise jest obiektem, gdzie callback, to przecież funkcja. Funkcja callback pozwala nam na obsłużenie zdarzenia, a promise to obiekt, który przechowuje pewien stan.

Spójrzmy na przykład prostej obietnicy:

new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});

Każdy promise pozwala zdefiniować funkcję (resolve), która wywołana zostanie w momencie, gdy osiągnięty zostanie stan fullfiled. W przeciwnym wypadku wykonana zostaje funkcja (reject), która obsługuje stan rejected.

Obietnice to obiekty, więc nie są one przekazywane jako parametry, jak funkcje callback – są zwracane. Najlepiej oddaje to zdanie:

The return statement is an object which is a placeholder for the result, which will be available in the future.

Obietnice biorą odpowiedzialność wyłącznie za jedno zdarzenie, gdzie funkcje callback obsługiwać mogą ich wiele, wielokrotnie.

Poprzedni przykład możemy nieco rozbudować i przypisać zwrócony obiekt promise do wyrażenia let:

let promise = new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});

Jak już wyżej wspomniałem – zwrócony obiekt udostępnia metodę then(), która pozwala obsłużyć wartość, w momencie, gdy promise przechodzi do stanu settled:

promise.then(onResolve, onReject)

Możemy użyć tej metody do obsłużenia zwracanych wartości w ramach funkcji onResolve, onReject lub obu.

let promise = new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});

promise.then(response => {
  console.log(response);
}, error => {
  console.log(error);
});

// success

W naszym przykładzie funkcja onReject nigdy się nie wykona i błąd nie zostanie rzucony. Możemy więc usunąć jej obsługę:

let promise = new Promise(resolve => {
  let value = 'success';
  resolve(value);
});

promise.then(response => {
  console.log(response); // success
});

W przypadku wywołania metody then(), obsługiwana jest nie tylko wartość danej obietnicy, zwracany jest również nowy obiekt Promise:

let promise = new Promise(resolve => {
  let value = 'success';
  resolve(value);
});

promise.then(response => {
  console.log(response); // success
  return 'another success';
}).then(response => {
  console.log(response); // another success 
});

Tworząc kod w ten sposób pozostaje on zawsze “płaski”. Nie budujemy tutaj piramid, więc termin callback hell może odejść w niepamięć.

W momencie, gdy interesuje nas tylko obsłużenie wyjątku (stan rejected), wystarczy, że podamy funkcję onReject do metody then():

let promise = new Promise((resolve, reject) => {
  let reason = 'failure';
  reject(reason);
});

promise.then(
  null,
  error => {
    console.log(error); // failure
  }
);

Istnieje jednak lepszy sposób. Poza metodą then(), otrzymujemy także metodę catch(), która pozwala na obsługę sytuacji wyjątkowych:

let promise = new Promise((resolve, reject) => {
  let reason = 'failure';
  reject(reason);
});

promise.catch(err => {
  console.log(err); // failure
});

W momencie, gdy mamy kilka wystąpień metody then() po sobie, wyjątek przekazywany jest tak długo, aż nie zostanie on obsłużony:

let promise = new Promise(resolve => {
  resolve();
});

promise
  .then(response => {
    return 1;
  })
  .then(response => {
    throw new Error('failure');
  })
  .catch(error => {
    console.log(error.message); // failure
  });

W momencie gdy do obsłużenia mamy wiele zdarzeń i na dodatek zależy nam, by wykonać kod po skończeniu ich wszystkich, przychodzi nam z pomocą kolejna metoda: all(). Jest to statyczna metoda klasy Promise, która jako swój argument przyjmuje tablicę obietnic do obsłużenia.

let doSmth = new Promise(resolve => {
    resolve('doSmth');
  }),
  doSmthElse = new Promise(resolve => {
    resolve('doSmthElse');
  }),
  oneMore = new Promise(resolve => {
    resolve('oneMore'); 
  });

Promise.all([
    doSmth,
    doSmthElse,
    oneMore
  ])
  .then(response => {
    let [one, two, three] = response;
    console.log(one, two, three); // doSmth doSmthElse oneMore
  });

W ramach response otrzymujemy tablicę z kolejnymi obiektami promise.

Poza tym – mamy do dyspozycji jeszcze dwie przydatne metody:

  • Promise.resolve(value)  – zwraca promise w stanie fullfiled, który przypisuje wartość value lub zwraca value jeżeli jest to już obiekt typu promise
  • Promise.reject(value) – zwraca obiekt promise w stanie rejected, gdzie wartością jest value

uwaga!

Jak wszystko, również obietnice mają swoje słabe punkty. W momencie, gdy wyjątek wystąpi w metodzie then(), musi on zostać obsłużony w naszym kodzie. W przeciwnym wypadku zostanie on zignorowany!

70-480 – Wdrażanie wywołania zwrotnego

70-480

Odbieranie wiadomości z HTML5 WebSocket API; stosowanie kwerendy jQuery w celu realizacji wywołania AJAX; podłączanie zdarzenia; wdrażanie wywołania zwrotnego przy użyciu funkcji anonimowych; obsługa wskaźnika „this”.



Opis zagadnienia zawiera wiele elementów, które na pierwszy rzut oka mogą wydawać się zupełnie różne, ale to co ich łączy, to mechanizm callback. Jest to tak zwana funkcja zwrotna, która rejestrowana jest przez autora kodu do wykonania przez bibliotekę, której używa. Callback wykonywany jest w odpowiednim czasie przez bibliotekę, która nie wie nic o tym co się wydarzy. Wszystko zależy od ciała zarejestrowanej funkcji.

HTML5 WebSocket API

Egzamin 70-480 wymaga od nas znajomości API WebSocket, które pojawiło się wraz z HTML5. Specyfikacja definiuje założenia komunikacji full-duplex, czyli w dwie strony serwer <-> przeglądarka <-> serwer.

Zakładane są cztery typy obsługiwanych callback:

  • onopen – połączenie jest gotowe i można wysyłać wiadomości
  • onmessage – w momencie otrzymania wiadomości z serwera
  • onclose – w momencie zamknięcia połączenia
  • onerror – w momencie wystąpienia błędu

Pierwszym krokiem jest oczywiście utworzenie połączenia:

var connection = new WebSocket('ws://localhost/');

Następnie możemy zdefiniować odpowiednie callback:

connection.onopen = function () {
  connection.send('Ping');
};

connection.onmessage = function (e) {
  console.log('Server: ' + e.data);
};

connection.onclose = function(e) {
  console.log('Closed!');
};

connection.onerror = function (error) {
  console.log('WebSocket Error ' + error);
};

Każdy z callbacków wykona się w ściśle zdefiniowanym momencie. Decyduje o tym API WebSocket oraz komunikacja z serwerem.

Staram się skupiać na podstawach, a zarazem materiale, który w pełni pokrywa założenia egzaminu 70-480. Jeżeli ktoś chce dowiedzieć się nieco więcej na temat HTML5 WebSocket API, to odsyłam do artykułu na html5rocks.com.

jQuery – get, post, ajax

Biblioteki jQuery przedstawiać nikomu nie trzeba. Zyskała ona ogromną popularność społeczności, a to za sprawą wielu ułatwień i funkcji, które zwyczajnie ułatwiają życie. jQuery również świetnie radzi sobie z asynchronicznością oraz obsługą requestów typu AJAX.

Polegają one na wykonywaniu operacji komunikacji z serwerem w tle. Tym samym możemy pobierać dane oraz zmieniać interfejs strony bez przeładowania okna przeglądarki.

Przedstawię niżej trzy najbardziej popularne metody do zapytań typu AJAX, które znajdziemy w bibliotece jQuery.

$.get

Pozwala wykonywać asynchroniczne zapytania typu GET. Sygnatura metody wygląda następująco:

$.get(url [,data] [,success] [,dataType])

, gdzie poszczególne parametry to:

  • url – [String] – url pod jaki ma zostać wysłane zapytanie typu GET
  • data – [PlainObject | String] – obiekt lub string, które wysłane zostają razem z zapytaniem do serwera
  • success – [Function(data, textStatus, jqXHR)] – funkcja i zarazem nasz callback, który wykonany zostanie w momencie poprawnego obsłużenia odpowiedzi. Parametry, które zostaną przekazane do wywołania funkcji to:
    • data – [PlainObject] – zwrócone dane
    • textStatus – [String] – status odpowiedzi
    • jqXHR – [jqXHR] – odpowiednik XMLHTTPRequest
  • dataType – [String] – typ zwracanych danych – xml, json, script, html. Domyślnie ustalany na podstawie zwróconych danych.

Funkcja $.get jest odpowiednikiem (skrótem) dla funkcji $.ajax:

$.ajax({
  url: url,
  data: data,
  success: success,
  dataType: dataType
});

$.post

W zasadzie od funkcji $.get różni się tym, że tym razem wysyła na serwer asynchroniczne żądanie typu POST. Zestaw parametrów jest tutaj identyczny, a odpowiednikiem funkcji $.ajax jest:

$.ajax({
  type: "POST",
  url: url,
  data: data,
  success: success,
  dataType: dataType
});

$.ajax

Funkcja, która zawsze wywoływana jest pod spodem. Jest to najbardziej ogólna funkcja pozwalająca na definiowanie asynchronicznych zapytań. Jej pełna specyfikacja znajduje się na stronie jQuery. Posiada pokaźną liczbę parametrów i opisywanie ich wszystkich tutaj byłoby sporym nadmiarem materiału do przyswojenia.

callback i słowo kluczowe this

W zasadzie o czymś podobnym pisałem już we wpisie o wywoływaniu i obsłudze zdarzeń. Chodzi mianowicie o dostęp do kontekstu this wywołanej funkcji. Spójrzmy na przykład funkcji, która wykonuje callback na sam koniec swojego działania:

function callbackFn() {
  console.log(this);
}

function invoke(x, y, callback) {
  var xy = x + y;
  callback();  
}

invoke(1, 2, callbackFn);

Co będzie wynikiem console.log(this)? Na konsoli otrzymamy obiekt window. Jak przekazać kontekst funkcji wywołującej callback? No to kolejny przykład:

function callbackFn() {
  console.log(this);
}

function invoke(x, y, callback) {
  var xy = x + y;
  callback.call(xy);  
}

invoke(1, 2, callbackFn);

Zmiana nastąpiła w linii 6 poprzez użycie call. Tym razem wyliczona wartość xy została przekazana do funkcji callbackFn i stanowi jej kontekst (this).