Bret Cameron
Bret CameronDigital Consultant @ Concurrent Design Group

Przekształcanie kodu synchronicznego w asynchroniczny w JavaScript dzięki Promise

Sprawdź, jak można wykorzystać Promise w JavaScript do uzyskania asynchroniczności, przekształcając kod synchroniczny. Zobacz też jak działają async i await oraz jak sprawić by korzystać z Reduxa w sposób asynchroniczny.
10.04.20205 min
Przekształcanie kodu synchronicznego w asynchroniczny w JavaScript dzięki Promise

Im więcej wiem o tworzeniu stron internetowych, tym bardziej doceniam znaczenie kodu asynchronicznego. Poza statycznymi stronami internetowymi kod asynchroniczny staje się integralną częścią programowania. Praktycznie każda aplikacja webowa polega na wysyłaniu, odbieraniu i przetwarzaniu danych za pośrednictwem API.

Pisanie asynchronicznego kodu może się bardzo różnić od pisania zwykłego, synchronicznego JavaScriptu. W kodzie synchronicznym możesz sobie przede wszystkim na więcej pozwolić. Na przykład, kolejność działania w kodzie synchronicznym jest łatwiejsza do przewidzenia — nieskładny i niechlujny kod nadal może działać, nawet jeśli Twoi współpracownicy są z niego niezadowoleni.

Natomiast struktura i kolejność kodu asynchronicznego musi być o wiele bardziej solidna — w tym artykule zobaczymy, jak sprawić, żeby taka właśnie była. Przyjrzymy się trzem głównym systemom do pisania asynchronicznego kodu. Co więcej, podzielę się z Wami niektórymi sposobami na uczynienie funkcji synchronicznych asynchronicznymi.

Wywołania zwrotne, try i catch

W epoce początków języka JavaScript, wykonywanie wielu asynchronicznych operacji z rzędu spowodowałoby powstanie tak zwanej piramidy zagłady. Oto przykład takiej piramidy.

func1(function(result) {
  func2(result, function(newResult) {
    func3(newResult, function(finalResult) {
      func4(newResult, function(finalResult) {
        console.log(finalResult);
      }, failureCallback);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);


Sytuacja ta była również znana jako piekło callbacków. Wraz ze wzrostem liczby operacji asynchronicznych, zorientowanie się w sytuacji staje się bardzo trudne, bardzo szybko.

W prostych przypadkach, do obsługi niepowodzenia można użyć instrukcji try i catch.

try {
  asyncFunction();
} 
catch (err) {
  console.error(err);
}


Potrzeba wielu asynchronicznych działań może jednak szybko doprowadzić do jeszcze gorszego zamieszania.

try {
  func1();
  try {
    func2();
    try {
      func3();
    } catch {
      failureFunc1();
    }
  } catch {
    failureFunc2();
  }
} catch {
  failureFunc3();
}

Obietnice, Then oraz Catch

Poważna zmiana nastąpiła w ES6, wraz z wprowadzeniem nowego obiektu: Promise. Obiekt Promise reprezentuje zakończenie lub niepowodzenie operacji asynchronicznej i zwróconą wartość tej operacji.

Obietnicę (red. spolszczenie Promise) można utworzyć za pomocą nowego konstruktora — Promise(). Pobiera on funkcję z dwoma argumentami — resolve oraz reject — jak w poniższym przykładzie:

const foo = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('bar');
  }, 3000);
});


Jeśli spróbujemy wywołać plik console.log(foo), zanim obietnica zostanie rozwiązana lub odrzucona, to zobaczymy Promise{<pending>}.

Po zakończeniu akcji wywołanie console.log(foo) zwróci jednak obiekt Promise zawierający wartość: w tym przypadku będzie to Promise{<resolved>}: "bar".

Then oraz Catch

Aby wykonać kolejne operacje na rozwiązanej lub odrzuconej obietnicy, ES6 wprowadziło dwie nowe metody: then i catch. Można je powiązać z naszą pierwotną obietnicą. Jeśli, na przykład, chcemy uzyskać dostęp do wyniku Promise, to używamy:

foo
  .then(result => console.log(result)
  .catch(err => console.error(err);


then się uruchamia, jeśli Promise jest rozwiązane, a catch uruchomi się, jeśli Promise zostanie odrzucone. Metody te można łączyć w łańcuchy tyle razy, ile to konieczne. Na przykład, częsty wzorzec przy używaniu fetch do pobierania danych w JSON-ie wygląda następująco:

fetch(myRequest)
  .then(response => response.json())
  .then(data => {
    processData(data);
  });


json() używamy w pierwszej metodzie do odczytu i parsowania danych oraz żeby je zwrócić. W następnej metodzie możemy natomiast przetworzyć sparsowane dane JSON.

Async oraz Await

Kod asynchroniczny stał się jeszcze wygodniejszy w użyciu w ES8. Stało się to dzięki wprowadzeniu dwóch nowych słów kluczowych: async oraz await.

System ten nie otworzył żadnych nowych możliwości. Zapewnia on raczej warstwę abstrakcji (lub „lukru składniowego”), umożliwiając pisanie kodu asynchronicznego w bardzo podobny sposób do pisania kodu synchronicznego.

const foo = async () => {
  const result = await new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('bar');
  }, 3000)
  });
  console.log(result);
};


Możesz zdefiniować funkcję asynchroniczną za pomocą async function() {} lub - jak w powyższym przykładzie — za pomocą const myFunctionName = async () => {}.

Wewnątrz funkcji async możesz użyć słowa kluczowego await, by zatrzymać wykonanie do momentu, aż obietnica zostanie rozwiązana.

W tym przykładzie użyjemy fetch, aby wysłać żądanie GET w celu pobrania danych użytkownika za pomocą API Githuba. Nie jest konieczne jawne użycie obiektu Promise, ponieważ wynika to z metody fetch:

const getUserData = async (user) => {
  let response = await fetch(`https://api.github.com/users/${name}`);
  let data = await response.json();
  return data;
}


Jedynym problemem związanym ze składnią async/await jest to, że ze względu na podobieństwo do kodu synchronicznego, łatwo jest wpaść w synchroniczny sposób myślenia. Było to szczególnie ciężkie, gdy async oraz await były dla mnie czymś nowym, więc popełniłem błędy, zapominając, że mam do czynienia z obietnicami!

Zmiana funkcji synchronicznych w asynchroniczne

Weźmy funkcję synchroniczną, która zwraca sumę każdej wartości w tablicy.

function sum(arr) {
  return arr.reduce((x, y) => x + y);
};


Załóżmy, że nie chcemy, aby ta funkcja blokowała wykonywanie innego kodu JavaScript. Aby inny kod mógł w tym czasie działać, musimy przekształcić naszą funkcję w funkcję asynchroniczną. W tym celu musi ona zwrócić obietnicę. Od czasu ES8, najprostszym na to sposobem jest dodanie słowa kluczowego async:

async function sum(arr) {
  return arr.reduce((x, y) => x + y);
};


A co, jeśli chcemy mieć większą kontrolę nad wykonaniem naszej obietnicy? Pośrednio, słowo kluczowe async zamienia w obiekt Promise wszystko, co zwróci nasza funkcja, a więc następująca funkcja ma mniej więcej takie samo zachowanie jak ta powyżej:

const asyncSum = (arr) => {
  return new Promise((resolve, reject) => {
    resolve(arr.reduce((x, y) => x + y))
  });
};


Możemy następnie wywołać tę funkcję oraz użyć metod lub słów kluczowych opisanych powyżej, aby zdefiniować dalsze działania, w zależności od tego, czy nasza obietnica zostanie zakończona pomyślnie, czy też nie. Często dzieje się też tak, że jeśli używamy takiej biblioteki, jak React, możemy chcieć zaktualizować stan po zwróceniu naszego wyniku:

asyncSum(veryLargeArray)
  .then(result => {
    this.setState({ sum: result });
  });
  .catch(err => console.log(err));


Albo, użyć async oraz await dla tego samego rezultatu:

(async () => {
  const result = await asyncSum(veryLargeArray);
  this.setState({ sum: result });
})()

Bonus: asynchroniczny Redux

I na koniec, biorąc pod uwagę popularność Redux, pomyślałem, że wspomnę, jak przekształcić jego akcje (które domyślnie są synchroniczne) w asynchroniczne.

Po zainstalowaniu redux i react-redux konieczne będzie również zainstalowanie middleware'u, który pozwoli, by kreator akcji zwrócił funkcję, zamiast akcji. Najpopularniejszym rozwiązaniem jest tutaj redux-thunk. Aby go włączyć, użyj następującego kodu:

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const initialState = {};

const middleware = [thunk];

let store;

if (window.navigator.userAgent.includes('Chrome')) {
  store = createStore(rootReducer, initialState, compose(
    applyMiddleware(...middleware),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  ));
} else {
  store = createStore(rootReducer, initialState, compose(
    applyMiddleware(...middleware)
  ));
}

export default store;


Możesz wtedy zwracać funkcje oraz akcje. Oto przykładowa funkcja, która wysyła żądanie POST w celu utworzenia elementu:

import axios from 'axios';

export const createCourse = (item) => {
  return function (dispatch) {
    return axios.post('create', item).then(
      res => dispatch({ type: 'CREATE_ITEM', payload: res.data }),
      error => console.log(error)
    );
  };
};


Dzięki Redux Thunk akcja ta jest teraz „thenable”, co oznacza, że możemy wykonać kolejne akcje wtedy, i tylko wtedy, gdy wynik zostanie zwrócony z powodzeniem. Rzadko używam Redux bez implementacji redux-thunk!

<p>Loading...</p>