Wie man mit verschachtelten Rückrufen umgeht und die „Rückrufhölle“ vermeidet

JavaScript ist eine seltsame Sprache. Hin und wieder müssen Sie sich mit einem Rückruf befassen, der sich in einem anderen Rückruf befindet, der sich in einem weiteren Rückruf befindet.

Die Leute nennen dieses Muster liebevoll die Rückrufhölle .

Es sieht irgendwie so aus:

firstFunction(args, function() { secondFunction(args, function() { thirdFunction(args, function() { // And so on… }); }); });

Dies ist JavaScript für Sie. Es ist umwerfend, verschachtelte Rückrufe zu sehen, aber ich denke nicht, dass es eine „Hölle“ ist. Die "Hölle" kann beherrschbar sein, wenn Sie wissen, was Sie damit machen sollen.

Bei Rückrufen

Ich gehe davon aus, dass Sie wissen, was Rückrufe sind, wenn Sie diesen Artikel lesen. Wenn Sie dies nicht tun, lesen Sie bitte diesen Artikel, um eine Einführung in Rückrufe zu erhalten, bevor Sie fortfahren. Dort sprechen wir darüber, was Rückrufe sind und warum Sie sie in JavaScript verwenden.

Lösungen zur Rückrufhölle

Es gibt vier Lösungen, um die Hölle zurückzurufen:

  1. Schreiben Sie Kommentare
  2. Teilen Sie Funktionen in kleinere Funktionen auf
  3. Versprechen verwenden
  4. Verwenden von Async / await

Bevor wir uns mit den Lösungen befassen, wollen wir gemeinsam eine Callback-Hölle konstruieren. Warum? Denn es ist zu abstrakt , um zu sehen firstFunction, secondFunctionund thirdFunction. Wir wollen es konkretisieren.

Eine Rückrufhölle bauen

Stellen wir uns vor, wir versuchen einen Burger zu machen. Um einen Burger zu machen, müssen wir die folgenden Schritte ausführen:

  1. Holen Sie sich Zutaten (wir gehen davon aus, dass es sich um einen Rindfleischburger handelt)
  2. Das Rindfleisch kochen
  3. Holen Sie sich Burger Brötchen
  4. Legen Sie das gekochte Rindfleisch zwischen die Brötchen
  5. Den Burger servieren

Wenn diese Schritte synchron sind, sehen Sie eine Funktion, die dieser ähnelt:

const makeBurger = () => { const beef = getBeef(); const patty = cookBeef(beef); const buns = getBuns(); const burger = putBeefBetweenBuns(buns, beef); return burger; }; const burger = makeBurger(); serve(burger);

Nehmen wir in unserem Szenario jedoch an, wir können den Burger nicht selbst herstellen. Wir müssen einen Helfer in die Schritte einweisen, um den Burger zu machen. Nachdem wir den Helfer angewiesen haben, müssen wir warten, bis der Helfer fertig ist, bevor wir mit dem nächsten Schritt beginnen.

Wenn wir auf etwas in JavaScript warten möchten, müssen wir einen Rückruf verwenden. Um den Burger zu machen, müssen wir zuerst das Rindfleisch holen. Wir können das Rindfleisch erst kochen, nachdem wir das Rindfleisch bekommen haben.

const makeBurger = () => { getBeef(function(beef) { // We can only cook beef after we get it. }); };

Um das Rindfleisch zu kochen, müssen wir beefin die cookBeefFunktion übergehen . Ansonsten gibt es nichts zu kochen! Dann müssen wir warten, bis das Rindfleisch gekocht ist.

Sobald das Rindfleisch gekocht ist, bekommen wir Brötchen.

const makeBurger = () => { getBeef(function(beef) { cookBeef(beef, function(cookedBeef) { getBuns(function(buns) { // Put patty in bun }); }); }); };

Nachdem wir die Brötchen bekommen haben, müssen wir das Pastetchen zwischen die Brötchen legen. Hier entsteht ein Burger.

const makeBurger = () => { getBeef(function(beef) { cookBeef(beef, function(cookedBeef) { getBuns(function(buns) { putBeefBetweenBuns(buns, beef, function(burger) { // Serve the burger }); }); }); }); };

Endlich können wir den Burger servieren! Aber wir können nicht zurück burgeraus , makeBurgerweil es asynchron ist. Wir müssen einen Rückruf annehmen, um den Burger zu servieren.

const makeBurger = nextStep => { getBeef(function (beef) { cookBeef(beef, function (cookedBeef) { getBuns(function (buns) { putBeefBetweenBuns(buns, beef, function(burger) { nextStep(burger) }) }) }) }) } // Make and serve the burger makeBurger(function (burger) => { serve(burger) })

(Ich hatte Spaß daran, dieses Rückruf-Höllenbeispiel zu machen?).

Erste Lösung zur Rückrufhölle: Schreiben Sie Kommentare

Die makeBurgerRückrufhölle ist einfach zu verstehen. Wir können es lesen. Es sieht einfach nicht gut aus.

Wenn Sie makeBurgerzum ersten Mal lesen , denken Sie vielleicht: „Warum zum Teufel brauchen wir so viele Rückrufe, um einen Burger zu machen? Das ergibt keinen Sinn! “.

In einem solchen Fall möchten Sie Kommentare hinterlassen, um Ihren Code zu erläutern.

// Makes a burger // makeBurger contains four steps: // 1. Get beef // 2. Cook the beef // 3. Get buns for the burger // 4. Put the cooked beef between the buns // 5. Serve the burger (from the callback) // We use callbacks here because each step is asynchronous. // We have to wait for the helper to complete the one step // before we can start the next step const makeBurger = nextStep => { getBeef(function(beef) { cookBeef(beef, function(cookedBeef) { getBuns(function(buns) { putBeefBetweenBuns(buns, beef, function(burger) { nextStep(burger); }); }); }); }); };

Jetzt anstatt zu denken "wtf?!" Wenn Sie die Callback-Hölle sehen, bekommen Sie ein Verständnis dafür, warum sie so geschrieben werden muss.

Zweite Lösung für die Rückrufhölle: Teilen Sie die Rückrufe in verschiedene Funktionen auf

Unser Callback-Hell-Beispiel ist bereits ein Beispiel dafür. Lassen Sie mich Ihnen den schrittweisen Imperativcode zeigen, und Sie werden sehen, warum.

Für getBeefunseren ersten Rückruf müssen wir zum Kühlschrank gehen, um das Rindfleisch zu holen. Es gibt zwei Kühlschränke in der Küche. Wir müssen zum richtigen Kühlschrank gehen.

const getBeef = nextStep => { const fridge = leftFright; const beef = getBeefFromFridge(fridge); nextStep(beef); };

Um Rindfleisch zu kochen, müssen wir das Rindfleisch in einen Ofen geben; Drehen Sie den Ofen auf 200 Grad und warten Sie 20 Minuten.

const cookBeef = (beef, nextStep) => { const workInProgress = putBeefinOven(beef); setTimeout(function() { nextStep(workInProgress); }, 1000 * 60 * 20); };

Stellen Sie sich nun vor, Sie müssen jeden dieser Schritte in makeBurger... schreiben.

Ein konkretes Beispiel für die Aufteilung von Rückrufen in kleinere Funktionen finden Sie in diesem kleinen Abschnitt in meinem Rückrufartikel.

Dritte Lösung zur Rückrufhölle: Versprechen verwenden

Ich gehe davon aus, dass Sie wissen, was Versprechen sind. Wenn nicht, lesen Sie bitte diesen Artikel.

Versprechen können die Verwaltung von Rückrufen erheblich vereinfachen. Anstelle des verschachtelten Codes, den Sie oben sehen, haben Sie Folgendes:

const makeBurger = () => { return getBeef() .then(beef => cookBeef(beef)) .then(cookedBeef => getBuns(beef)) .then(bunsAndBeef => putBeefBetweenBuns(bunsAndBeef)); }; // Make and serve burger makeBurger().then(burger => serve(burger));

Wenn Sie den Einzelargumentstil mit Versprechungen nutzen, können Sie Folgendes anpassen:

const makeBurger = () => { return getBeef() .then(cookBeef) .then(getBuns) .then(putBeefBetweenBuns); }; // Make and serve burger makeBurger().then(serve);

Viel einfacher zu lesen und zu verwalten.

Die Frage ist jedoch, wie Sie Callback-basierten Code in Promise-basierten Code konvertieren.

Rückrufe in Versprechen umwandeln

Um Rückrufe in Versprechen umzuwandeln, müssen wir für jeden Rückruf ein neues Versprechen erstellen. Wir können resolveversprechen, wenn der Rückruf erfolgreich ist. Oder wir können rejectversprechen, wenn der Rückruf fehlschlägt.

const getBeefPromise = _ => { const fridge = leftFright; const beef = getBeefFromFridge(fridge); return new Promise((resolve, reject) => { if (beef) { resolve(beef); } else { reject(new Error(“No more beef!”)); } }); }; const cookBeefPromise = beef => { const workInProgress = putBeefinOven(beef); return new Promise((resolve, reject) => { setTimeout(function() { resolve(workInProgress); }, 1000 * 60 * 20); }); };

In der Praxis würden Rückrufe wahrscheinlich bereits für Sie geschrieben. Wenn Sie Node verwenden, hat jede Funktion, die einen Rückruf enthält, dieselbe Syntax:

  1. Der Rückruf wäre das letzte Argument
  2. Der Rückruf hat immer zwei Argumente. Und diese Argumente sind in derselben Reihenfolge. (Fehler zuerst, gefolgt von allem, was Sie interessiert).
// The function that’s defined for you const functionName = (arg1, arg2, callback) => { // Do stuff here callback(err, stuff); }; // How you use the function functionName(arg1, arg2, (err, stuff) => { if (err) { console.error(err); } // Do stuff });

Wenn Ihr Rückruf dieselbe Syntax hat, können Sie Bibliotheken wie ES6 Promisify oder Denodeify (de-node-ify) verwenden, die diesen Rückruf in ein Versprechen umwandeln. Wenn Sie Node v8.0 und höher verwenden, können Sie util.promisify verwenden.

All three of them work. You can choose any library to work with. There are slight nuances between each method, though. I’ll leave you to check their documentation for how-tos.

Fourth solution to callback hell: Use asynchronous functions

To use asynchronous functions, you need to know two things first:

  1. How to convert callbacks into promises (read above)
  2. How to use asynchronous functions (read this if you need help).

With asynchronous functions, you can write makeBurger as if it’s synchronous again!

const makeBurger = async () => { const beef = await getBeef(); const cookedBeef = await cookBeef(beef); const buns = await getBuns(); const burger = await putBeefBetweenBuns(cookedBeef, buns); return burger; }; // Make and serve burger makeBurger().then(serve);

There’s one improvement we can make to the makeBurger here. You can probably get two helpers to getBuns and getBeef at the same time. This means you can await them both with Promise.all.

const makeBurger = async () => { const [beef, buns] = await Promise.all(getBeef, getBuns); const cookedBeef = await cookBeef(beef); const burger = await putBeefBetweenBuns(cookedBeef, buns); return burger; }; // Make and serve burger makeBurger().then(serve);

(Note: You can do the same with Promises… but the syntax isn’t as nice and as clear as async/await functions).

Wrapping up

Callback hell isn’t as hellish as you think. There are four easy ways to manage callback hell:

  1. Write comments
  2. Split functions into smaller functions
  3. Using Promises
  4. Using Async/await

This article was originally posted on my blog.

Sign up for my newsletter if you want more articles to help you become a better frontend developer.