🎆 Promises 🎊

Prevedeno/ukradeno sa https://www.youtube.com/watch?v=hf1T_AONQJU.

Exhibit 🅰️

function getUserSync(name) {
  const response = ajaxSync(`/user/${name}`);
  const { user } = response;
  if (!user) throw new Error('User not found');
  return user;
}

Exhibit 🅱️

function getUserAsync(name, callback) {
  ajax(
    `/user/${name}`,
    response => {
      const { user } = response;
      if (!user) {
        callback(new Error('User not found'));
      } else {
        callback(null, user);
      }
    },
    error => callback(error),
  );
}
function getUserSync(name) {
  const response = ajaxSync(`/user/${name}`);
  const { user } = response;
  if (!user) throw new Error('User not found');
  return user;
}
function getUserAsync(name, callback) {
  ajax(
    `/user/${name}`,
    response => {
      const { user } = response;
      if (!user) {
        callback(new Error('User not found'));
      } else {
        callback(null, user);
      }
    },
    error => callback(error),
  );
}

Async callback problemi

Funkcije mogu raditi tri stvari:
1️⃣ Vratiti vrijednost
2️⃣ Baciti iznimku
3️⃣ Side effect

Sa callback funkcijama izgubili smo 1️⃣ i 2️⃣ te se oslanjamo samo na 3️⃣

Još problema

// Pronađite bug!
function getUsers(names, callback) {
  const users = [];

  names.forEach(name => {
    ajax(
      `/user/${name}`,
      ({ user }) => {
        if (!user) {
          callback(new Error('User not found'));
        } else {
          users.push(user);
          if (users.length === names.length) {
            callback(null, users);
          }
        }
      },
      error => callback(error),
    );
  });
}

Async callback trade-off

  • Izgubili smo return 🐟
  • Izgubili smo throw 🐡
  • Izgubili smo Stack 🐠
  • Ne znamo hoće li callback biti pozvan više puta i sa različitim argumentima 🦀
    • Sync funkcije mogu vratiti najviše jednu vrijednost
    • Sync funkcije mogu baciti najviše jednu iznimku
  • Ali naša funkcija je sad non-blocking 🎉

Promise

  • aka future, delay ili deferred
  • uvedeni u ECMAScript 2015
  • Promise predstavlja asinkronu operaciju
  • Rezultat operacije nije nužno poznat u trenutku kada je Promise objekt stvoren

Ilustracija*

  • Kolega 🐢 je obećao predavanje, tema: Event loop & Promises
  • Dao je to obećanje kolegama
  • Sa danim obećanjem kolege barataju kako god žele:
    • Kolega 🐰 kaže da će prijeći u frontend tim nakon izvršenog obećanja (✅)
    • Kolega 🐘 sumnja da će 🐢 izvršiti obećanje, ako ne uspije dati će mu 🐟 (❌)
    • Kolegica 🐝 je obećala svom prijatelju da će mu, kad posluša to predavanje, objasniti kako Promise radi (✅)
  • Svi kolege mogu nastaviti sa drugim poslovima dok obećanje nije izvršeno/odbačeno

* svaka sličnost sa stvarnim osobama je slučajna

Stanja Promise objekta

  • ⏰ pending - u tijeku
    • Početno stanje
    • Jedino koje može prijeći u neko drugo stanje
  • ✅ fulfilled - ispunjeno
    • Operacija je uspješno završena, s najviše jednom vrijednošću
    • Vrijednost se neće promijeniti
  • ❌ rejected - odbačeno
    • Operacija neuspješno završena, opcionalno s nekim razlogom (error)
    • Razlog se neće promijeniti

Metode Promise objekta

.then(onFulfilled, onRejected)

  • Prima dva handler argumenta (ignoriraju se ako nisu funkcije)
    • onFulfilled - izvršava se kada je obećanje ispunjeno
    • onRejected - izvršava se kada je obećanje odbačeno
  • Vraća novi Promise objekt ovisno o tome što se dogodilo u handler-ima

.catch(onRejected)

  • Ekvivalent aPromise.then(undefined, onRejected)

Primjer stvaranja

Pretvorba getUserAsync() funkcije iz primjera 🅱️ u funkciju koja vraća Promise:

function getUserPromise(name) {
  return new Promise((resolve, reject) => {
    getUserAsync(name, (error, user) => {
      if (error) {
        reject(error);
      } else {
        resolve(user);
      }
    });
  });
}

Konverzija poziva callback funkcije u Promise funkciju

getUser('ldgit', (error, user) => {
  // ...
});

Postaje

getUser('ldgit').then(
  user => {
    // ...
  },
  error => {
    // ...
  },
);

Brzi zadaci

S kojom vrijednošću su ispunjeni/odbijeni ovi Promise objekti?

new Promise((resolve, reject) => {
  resolve(5);
  reject(new Error('Ne valja'));
});
new Promise((resolve, reject) => {
  reject(new Error('Stani!'));
  resolve('Riješeno');
});
new Promise((resolve, reject) => {
  resolve(5);
  resolve('Riješeno');
});

Promise/Synchronous paralele

  • onFulfilled i onRejected:
    • Izvršit će se najviše jednom
    • S najviše jednom vrijednošću/greškom
  • Promise se može ispuniti ili odbaciti samo jednom: daljni resolve i reject pozivi nakon prvoga ne rade ništa

❗Paralele su return i throw ❗

Promise/Synchronous paralele

⚡ Cilj Promise-a je dobiti nazad mogućnosti sinkronog kôda u async funkcijama. ⚡

Ponašanje onFulfilled i onRejected handlera pokriva četiri osnovna scenarija, ovisno o stanju Promise objekta:

  1. Ispunjeno obećanje, onFullfiled vraća vrijednost <=> jednostavna funkcionalna transformacija
  2. Ispunjeno obećanje, onFullfiled baca iznimku <=> bacanje iznimke kada dobijemo neispravnu vrijednost
  3. Odbačeno obećanje, onRejected vraća vrijednost <=> catch unutar kojeg smo handle-ali grešku
  4. Odbačeno obećanje, onRejected baca iznimku <=> catch unutar kojeg smo ponovo bacili istu (ili novu) grešku

Sync patterni sa Promise objektima 1️⃣

Dohvaćanje vrijednosti, bacanje iznimke ako vrijednost ne postoji:

const user = getUser('ldgit');
if (!user) {
  throw new Error('User not found');
}
const name = user.name;

Promise verzija:

getUser('ldgit').then(user => {
  if (!user) {
    throw new Error('User not found');
  }
  return user.name;
});

Sync patterni sa Promise objektima 2️⃣

Obrada iznimki:

try {
  notifyUser('ldgit');
} catch (error) {
  handleError(error);
}

Promise verzija:

notifyUser('ldgit').then(undefined, error => {
  handleError(error);
});

Sync patterni sa Promise objektima 3️⃣

Re-throw iznimki:

try {
  notifyUser('ldgit');
} catch (error) {
  throw new Error(`Došlo je do greške: ${error.message}`);
}

Promise verzija:

notifyUser('ldgit').then(undefined, error => {
  throw new Error(`Došlo je do greške: ${error.message}`);
});

Niz operacija

// Sinkroni kôd
const user = getUser('ldgit');
const reviews = getReviews(user);
renderReviews(reviews);
// Callbacks
getUser('ldgit', user => {
  getReviews(user, reviews => {
    renderReviews(reviews);
  });
});
// Promises
getUser('ldgit').then(getReviews).then(renderReviews);

Baratanje iznimkama

Sinkroni kôd

try {
  const user = getUser('ldgit');
  const reviews = getReviews(user);
  renderReviews(reviews);
} catch (error) {
  handleError(error);
}

Baratanje iznimkama

Callbacks 🌋

getUser('ldgit', (error, user) => {
  if (error) {
    handleError(error);
  } else {
    getReviews(user, (error, reviews) => {
      if (error) {
        handleError(error);
      } else {
        renderReviews(reviews);
      }
    });
  }
});

Baratanje iznimkama

Promises

getUser('ldgit')
  .then(getReviews)
  .then(renderReviews)
  .then(undefined, handleError);

  ☀️     🐦

🌲🌿🐇🌳🌼

Paralelne asinkrone operacije

Postižemo koristeći Promise.all(iterable)

Prima (najčešće) array Promise objekata, a vraća novi Promise koji je:

  • Ispunjen kada su svi Promise objekti ispunjeni
  • Odbačen ako je bilo koji u nizu promise-a odbačen
    • Greška je greška prvog odbačenog promise objekta

Konverzija getUsers funkcije iz ranijeg primjera:

function getUsers(names) {
  const userPromises = names.map(name => {
    // ajax funkcija je asinkrona i vraća promise
    return ajax(`/user/${name}`).then(({ user }) => {
      if (!user) {
        throw new Error('User not found');
      }
      return user;
    });
  });

  return Promise.all(userPromises);
}

Budućnost 🚀

Async Await 🎉

Async funkcije

  • Označene su async prefiksom
  • Implicitno vraćaju Promise objekt
  • I arrow funkcije mogu biti async

Async funkcije

Bilo koju običnu funkcije možemo pretvoriti u asinkronu ako joj dodamo async prefix:

function getFishCount() {
  return 42; // vraća integer 42
}

async function getFishCount() {
  return 42; // vraća Promise koji se resolve-a sa 42
}

Await

  • Ključna riječ await se može staviti ispred bilo kojeg poziva asinkrone funkcije
  • Ako await koristimo unutar funkcije, ta funkcija mora biti označena kao asinkrona kroz async
  • Pauzira izvođenje funkcije unutar koje se nalazi dok se vraćeni Promise ne ispuni

Promise chain napisan preko Async Await (sync verzija ovdje)

try {
  const user = await getUser('ldgit');
  const reviews = await getReviews(user);
  renderReviews(reviews);
} catch (error) {
  handleError(error);
}

Unutar funkcije:

async function getReviewsForUser(username) {
  const user = await getUser(username);
  return await getReviews(user);
}

renderReviews(await getReviewsForUser('ldgit'));

Izvori

For fun

  • Stvarno cool test suite s kojim i vi možete napisati pravu Promise implementaciju.

Sinkrona funkcija: tražimo korisnika preko sync ajax poziva i ako ga nema bacamo iznimku

Verzija iste funkcije sa async callback funkcijama. Argumenti callback funkcije (error, result) su bazirani na Node.js konvenciji: - ako je došlo do greške, callback se poziva samo sa greškom kao prvim argumentom - ako je pronađen rezultat, callback se poziva sa `null` kao prvim argumentom i rezultatom kao drugim argumentom

Možemo primjetiti kako async funkciji fale dvije ključne riječi koje sinkrona funkcija ima: - return - throw

Sync verzija funkcije getUsers(): - dohvaća korisnike sa zadanim imenima - svi korisnici moraju postojati, ako ijedan fali bacamo iznimku greška! Bug: ako pošaljemo dva imena za koje nema korisnika, callback će biti pozvan dva puta Čak i ako bacimo iznimku ne možemo prekinuti izvođenje funkcije, ostali callbacks će se nastaviti izvoditi.

Pod gubitkom stacka se misli i na mogućnost da bacanje iznimke prekine izvođenje funkcije

Ne znamo koliko će se puta callback pozvati - sync funkcija vrati vrijednost najviše jednom

:rabbit: je postavio onFulfilled handler

:elephant: je postavio onRejected handler

:honeybee: je postavila onFulfilled handler koji stvara novo obećanje i poslala to obećanje svom prijatelju, koji na to obećanje može postaviti svoj onFulfilled/onRejected handler, itd...

Svi kolege mogu nastaviti sa drugim poslovima jer obećanje predstavlja rezultat asinkrone operacije

Ako :turtle: kaže da neće izvršiti, ni obećanje koje je :honeybee: dala prijatelju neće biti ispunjeno, osim ako nije postavila i error handler u kojem bi npr. naučila o Promise-ima sa interneta

Pod "neće se promijeniti" se *ne misli* na duboku nepromjenjivost (npr. ako je objekt, property-ji se mogu promijeniti). Analogno varijabli definiranoj sa `const`.

onFulfilled će se izvršiti čak i ako je obećanje već ispunjeno u trenutku kada smo pozvali then()

Analogno vrijedi i za onRejected

Svaku async callback funkciju možemo vrlo jednostavno pretvoriti u funkciju koja vraća Promise.

Callback koji šaljemo u `new Promise` se izvršava odmah - pozivom resolve metode pozvati će se svi zakačeni onFulfilled handleri - pozivom reject metode pozvati će se svi zakačeni onRejected handleri

Prvi je ispunjen sa vrijednošću 5, error handler se neće pozvati

Drugi je odbijen sa greškom "Stani!", success handler se neće pozvati

Treći je ispunjen sa vrijednošću 5, success handler će pozvati samo jednom

Async kôd sada funkcionira vrlo slično sync kôdu: - `return user.name` će vratiti novi promise koji ispunjava sa korisnikovim imenom - ako je došlo do greške, `throw` će vratiti novi promise koji je odbačen sa greškom - vraćenu vrijednost, ili bačenu grešku, imamo u idućem .then() pozivu: unutar success handlera, ili error handlera u slučaju greške

Sve operacije iz sinkronog svijeta su nam dostupne i u asinkronom sa Promise-ima

Sa Promises smo na određeni način dobili nazad stack: greške koje bacimo duboko u stack-u će "isplivati" na vrh, do prvog catch-a ili onRejected handlera

Promise.all() prima array promise objekata, a vraća novi promise koji: - je ispunjen ako su *svi* promise objekti ispunjeni - oodbačen ako je bilo koji u nizu promise-a odbačen, greška je greška prvog odbačenog promise objekta

Ako koristimo await unutar ne-async funkcije dobiti ćemo sintaksnu grešku