Jak se vypořádat s vnořenými zpětnými voláními a vyhnout se „peklu zpětného volání“

Foto: Jefferson Santos, Unsplash

JavaScript je zvláštní jazyk. Jednou za čas se musíte vypořádat s zpětným volaním, které je v jiném zpětném volání, které je v jiném zpětném volání.

Lidé laskavě nazývají tento vzorec peklem zpětného volání.

Vypadá to takto:

firstFunction (args, function () {
  secondFunction (args, function () {
    thirdFunction (args, function () {
      // A tak dále…
    });
  });
});

Toto je pro vás JavaScript. Je ohromující vidět vnořená zpětná volání, ale nemyslím si, že je to „peklo“. „Peklo“ může být zvládnutelné, pokud víte, co s tím dělat.

Na zpětná volání

Předpokládám, že víte, co jsou zpětná volání, pokud čtete tento článek. Pokud tak neučiníte, přečtěte si v tomto článku úvod do zpětného volání před pokračováním. Tam mluvíme o tom, co jsou zpětná volání a proč je používáte v JavaScriptu.

Řešení pro zpětné zavolání

Existují čtyři řešení, jak zavolat peklo:

  1. Napište komentáře
  2. Rozdělte funkce na menší funkce
  3. Používání příslibů
  4. Použití Async / čekejte

Než se pustíme do řešení, vytvořme spolu peklo zpětného volání. Proč? Protože je příliš abstraktní na to, aby se daly vidět první funkce, druhá funkce a třetí funkce. Chceme, aby to bylo konkrétní.

Stavíme peklo zpětného volání

Představme si, že se snažíme udělat hamburger. Chcete-li udělat hamburger, musíme projít následující kroky:

  1. Získejte ingredience (budeme předpokládat, že je to hovězí hamburger)
  2. Vařte hovězí maso
  3. Získejte hamburgery
  4. Vložte vařené hovězí maso mezi housky
  5. Podávejte hamburger

Pokud jsou tyto kroky synchronní, podíváte se na funkci, která se podobá této:

const makeBurger = () => {
  const beef = getBeef ();
  const patty = cookBeef (hovězí);
  const buchty = getBuns ();
  const burger = putBeefBetweenBuns (housky, hovězí maso);
  návratový hamburger;
};
const burger = makeBurger ();
sloužit (hamburger);

V našem scénáři však řekněme, že si nemůžeme udělat hamburger sami. Musíme připravit pomocníka na kroky, aby se hamburger stal. Poté, co instruujeme pomocníka, musíme počkat, až pomocník skončí, než začneme další krok.

Pokud chceme na JavaScript něco čekat, musíme použít zpětné volání. Abychom si udělali hamburger, musíme nejprve dostat hovězí maso. Můžeme vařit hovězí maso až poté, co dostaneme hovězí maso.

const makeBurger = () => {
  getBeef (funkce (hovězí)) {
    // Můžeme vařit hovězí maso až poté, co to dostaneme.
  });
};

Chcete-li vařit hovězí maso, musíme předat hovězí maso do funkce cookBeef. Jinak není co vařit! Pak musíme počkat, až se hovězí maso uvaří.

Jakmile se hovězí maso uvaří, dostaneme buchty.

const makeBurger = () => {
  getBeef (funkce (hovězí)) {
    cookBeef (hovězí maso, funkce (cookedBeef) {
      getBuns (funkce (housky) {
        // Vložte patty do housky
      });
    });
  });
};

Poté, co dostaneme buchty, musíme vložit patty mezi buchty. Zde se formuje hamburger.

const makeBurger = () => {
  getBeef (funkce (hovězí)) {
    cookBeef (hovězí maso, funkce (cookedBeef) {
      getBuns (funkce (housky) {
        putBeefBetweenBuns (housky, hovězí maso, funkce (hamburger) {
            // Podávejte hamburger
        });
      });
    });
  });
};

Nakonec můžeme sloužit hamburger! Ale nemůžeme vrátit hamburger z makeBurgeru, protože je asynchronní. Abychom sloužili hamburgeru, musíme přijmout zpětné volání.

const makeBurger = nextStep => {
  getBeef (funkce (hovězí)) {
    cookBeef (hovězí maso, funkce (cookedBeef) {
      getBuns (funkce (housky) {
        putBeefBetweenBuns (housky, hovězí maso, funkce (hamburger) {
          nextStep (burger)
        })
      })
    })
  })
}
// Připravte a podávejte hamburger
makeBurger (funkce (hamburger) => {
  sloužit (hamburger)
})

(Bavil jsem se dělat tento příklad pekla zpětného volání ).

První řešení pro zpětné volání: Napište komentáře

Peklo zpětného volání makeBurger je snadno pochopitelné. Můžeme to přečíst. Prostě ... nevypadá hezky.

Pokud čtete makeBurger poprvé, možná si pomyslíte: „Proč k čertu potřebujeme tolik zpětných volání, aby se hamburger stal? To nedává smysl! “.

V takovém případě byste chtěli nechat komentáře vysvětlit svůj kód.

// Dělá hamburger
// makeBurger obsahuje čtyři kroky:
// 1. Získejte hovězí maso
// 2. Vařte hovězí maso
// 3. Získejte housky pro hamburger
// 4. Vařené hovězí maso vložte mezi housky
// 5. Podávejte hamburger (z zpětného volání)
// Používáme zpětná volání, protože každý krok je asynchronní.
// Musíme počkat, až pomocník dokončí jeden krok
// než začneme další krok
const makeBurger = nextStep => {
  getBeef (funkce (hovězí)) {
    cookBeef (hovězí maso, funkce (cookedBeef) {
      getBuns (funkce (housky) {
        putBeefBetweenBuns (housky, hovězí maso, funkce (hamburger) {
          nextStep (burger);
        });
      });
    });
  });
};

Nyní, místo toho, abyste přemýšleli o „wtf ?!“, když uvidíte peklo zpětného volání, pochopíte, proč to musí být napsáno tímto způsobem.

Druhé řešení pro peklo zpětného volání: Rozdělte zpětná volání do různých funkcí

Náš příklad pekla zpětného volání je již příkladem. Dovolte mi ukázat vám krok za krokem imperativní kód a uvidíte, proč.

Pro getBeef, náš první zpětný hovor, musíme jít do lednice, abychom dostali hovězí maso. V kuchyni jsou dvě ledničky. Musíme jít do správné ledničky.

const getBeef = nextStep => {
  const chladnička = leftFright;
  const beef = getBeefFromFridge (lednička);
  nextStep (hovězí);
};

K vaření hovězího masa musíme dát hovězí maso do trouby; otočte troubu na 200 stupňů a počkejte dvacet minut.

const cookBeef = (hovězí maso, nextStep) => {
  const workInProgress = putBeefinOven (hovězí);
  setTimeout (function () {
    nextStep (workInProgress);
  }, 1 000 * 60 * 20);
};

Teď si představte, jestli musíte v makeBurgeru napsat každý z těchto kroků ... pravděpodobně zmizí z pouhého množství kódu!

Pro konkrétní příklad rozdělení rozdělení zpětných volání na menší funkce si můžete přečíst tuto malou sekci v mém článku zpětného volání.

Třetí řešení pro zpětné zavolání: Používejte sliby

Budu předpokládat, že víš, jaké sliby jsou. Pokud tak neučiníte, přečtěte si tento článek.

Sliby mohou mnohem jednodušší správu zpětného volání. Namísto vnořeného kódu, který vidíte výše, budete mít toto:

const makeBurger = () => {
  návrat getBeef ()
    .then (hovězí => cookBeef (hovězí))
    .then (cookedBeef => getBuns (hovězí))
    .then (bunsAndBeef => putBeefBetweenBuns (bunsAndBeef));
};
// Připravte a podávejte hamburger
makeBurger (). poté (burger => sloužit (burger));

Pokud využijete styl s jedním argumentem s příslibem, můžete výše uvedené vylepšit:

const makeBurger = () => {
  návrat getBeef ()
    .then (cookBeef)
    .then (getBuns)
    .then (putBeefBetweenBuns);
};
// Připravte a podávejte hamburger
makeBurger (). then (serve);

Mnohem snazší čtení a správa.

Otázkou však je, jak převést kód založený na zpětném volání na kód založený na slibu.

Převod zpětných volání na sliby

Chcete-li převést zpětná volání na přísliby, musíme pro každý zpětné volání vytvořit nový příslib. Když je zpětné volání úspěšné, můžeme tento slib vyřešit. Nebo můžeme příslib odmítnout, pokud zpětné volání selže.

const getBeefPromise = _ => {
  const chladnička = leftFright;
  const beef = getBeefFromFridge (lednička);
  vrátit nový Promise ((vyřešit, odmítnout) => {
    if (hovězí) {
      rozhodnutí (hovězí);
    } jinde {
      odmítnout (nová chyba („Už žádné hovězí maso!“));
    }
  });
};
const cookBeefPromise = hovězí maso => ​​{
  const workInProgress = putBeefinOven (hovězí);
  vrátit nový Promise ((vyřešit, odmítnout) => {
    setTimeout (function () {
      vyřešit (workInProgress);
    }, 1 000 * 60 * 20);
  });
};

V praxi by pro vás pravděpodobně již byla napsána zpětná volání. Pokud používáte uzel, každá funkce, která obsahuje zpětné volání, bude mít stejnou syntaxi:

  1. Zpětné volání by bylo posledním argumentem
  2. Zpětné volání bude mít vždy dva argumenty. A tyto argumenty jsou ve stejném pořadí. (Nejdříve chyba, následovaná tím, co vás zajímá).
// Funkce, která je pro vás definována
const functionName = (arg1, arg2, callback) => {
  // Dělej tady věci
  zpětné volání (err, věci);
};
// Jak tuto funkci používáte
functionName (arg1, arg2, (err, stuff) => {
  if (err) {
  console.error (err);
  }
  // Dělat věci
});

Pokud má vaše zpětné volání stejnou syntaxi, můžete použít knihovny jako ES6 Promisify nebo Denodeify (de-node-ify), které zpětné volání zahrnují do slibu. Pokud používáte uzel v8.0 a vyšší, můžete použít util.promify.

Všichni tři pracují. Můžete si vybrat libovolnou knihovnu, se kterou budete pracovat. Mezi jednotlivými metodami však existují mírné nuance. Nechám vás, abyste zkontrolovali jejich dokumentaci, jak na to.

Čtvrté řešení pro zpětné volání: Použijte asynchronní funkce

Chcete-li používat asynchronní funkce, musíte nejprve vědět dvě věci:

  1. Jak převést zpětná volání na sliby (viz výše)
  2. Jak používat asynchronní funkce (přečtěte si, pokud potřebujete pomoc).

S asynchronními funkcemi můžete psát makeBurger, jako by byl znovu synchronní!

const makeBurger = async () => {
  const beef = await getBeef ();
  const cookedBeef = čeká cookBeef (hovězí);
  const buchty = await getBuns ();
  const burger = await putBeefBetweenBuns (vařené hovězí maso, housky);
  návratový hamburger;
};
// Připravte a podávejte hamburger
makeBurger (). then (serve);

Zde je jedno vylepšení, které můžeme udělat pro výrobce hamburgerů. Pravděpodobně můžete získat dva pomocníky pro getBuns a getBeef současně. To znamená, že na ně můžete čekat pomocí Promise.all.

const makeBurger = async () => {
  const [hovězí maso, housky] = čekejte na Promise.all (getBeef, getBuns);
  const cookedBeef = čeká cookBeef (hovězí);
  const burger = await putBeefBetweenBuns (vařené hovězí maso, housky);
  návratový hamburger;
};
// Připravte a podávejte hamburger
makeBurger (). then (serve);

(Poznámka: To samé můžete udělat s Promises… ale syntaxe není tak hezká a jasná jako funkce asynchronizace / čekání).

Zabalení

Callback peklo není tak pekelné, jak si myslíte. Existují čtyři jednoduché způsoby, jak spravovat peklo zpětného volání:

  1. Napište komentáře
  2. Rozdělte funkce na menší funkce
  3. Používání příslibů
  4. Použití Async / čekejte

Tento článek byl původně zveřejněn na mém blogu.
Pokud se chcete dozvědět více článků, které vám pomohou stát se lepším vývojářem frontendu, přihlaste se k odběru novinek.