Як боротися з вкладеними зворотними викликами та уникати “пекла зворотного виклику”

JavaScript - це дивна мова. Час від часу вам доводиться мати справу зі зворотним дзвінком, який знаходиться в іншому зворотному дзвінку, який є ще одним зворотним дзвінком.

Люди ласкаво називають цей шаблон зворотним дзвінком пеклом .

Це виглядає приблизно так:

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

Це JavaScript для вас. Дивовижно бачити вкладені зворотні виклики, але я не думаю, що це “пекло”. “Пекло” може бути керованим, якщо ви знаєте, що з ним робити.

Про зворотні дзвінки

Припускаю, ви знаєте, що таке зворотні дзвінки, якщо ви читаєте цю статтю. Якщо цього не сталося, прочитайте цю статтю, щоб ознайомитись із зворотними викликами, перш ніж продовжувати. Там ми говоримо про те, що таке зворотні виклики та чому ви використовуєте їх у JavaScript.

Рішення пекло зворотного дзвінка

Є чотири рішення пекла зворотного виклику:

  1. Пишіть коментарі
  2. Розбийте функції на менші функції
  3. Використання обіцянок
  4. Використання Async / await

Перш ніж заглибитися в рішення, давайте побудуємо пекло зворотного виклику разом. Чому? Тому що це занадто абстрактно , щоб бачити firstFunction, secondFunctionі thirdFunction. Ми хочемо зробити це конкретним.

Побудова пекла зворотного дзвінка

Уявімо, що ми намагаємось приготувати гамбургер. Щоб приготувати гамбургер, нам потрібно пройти такі дії:

  1. Отримайте інгредієнти (ми припустимо, що це яловичий гамбургер)
  2. Зварити яловичину
  3. Отримайте булочки з гамбургерами
  4. Помістіть між булочок приготовлену яловичину
  5. Подавайте гамбургер

Якщо ці кроки синхронні, ви будете розглядати функцію, яка нагадує цю:

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

Однак, у нашому сценарії, скажімо, ми не можемо самостійно приготувати гамбургер. Ми маємо доручити помічнику на сходах зробити гамбургер. Після того, як ми вкажемо помічнику, ми повинні ЗАЧЕКАЙТИ, поки помічник закінчить, перш ніж ми почнемо наступний крок.

Якщо ми хочемо чекати чогось у JavaScript, нам потрібно використовувати зворотний виклик. Щоб приготувати гамбургер, спочатку нам потрібно отримати яловичину. Готувати яловичину ми можемо лише після того, як отримаємо яловичину.

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

Щоб приготувати яловичину, нам потрібно перейти beefдо cookBeefфункції. Інакше готувати нема чого! Тоді нам потрібно почекати, поки яловичина приготується.

Як тільки яловичина готується, ми отримуємо булочки.

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

Після того, як ми дістанемо булочки, нам потрібно покласти пиріжок між булочками. Тут формується гамбургер.

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

Нарешті, ми можемо подати гамбургер! Але ми не можемо повернутися burgerз, makeBurgerбо це асинхронно. Нам потрібно прийняти зворотний дзвінок, щоб подати гамбургер.

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) })

(Мені було весело, роблячи цей приклад пекло зворотного дзвінка?).

Перше рішення пекла зворотного дзвінка: Напишіть коментарі

makeBurgerЯк передзвонити пекло просто зрозуміти. Ми можемо це прочитати. Це просто ... не виглядає приємно.

Якщо ви читаєте makeBurgerвперше, ви можете подумати: «Чому, блін, нам потрібно стільки зворотних дзвінків, щоб зробити гамбургер? Це не має сенсу! ”.

У такому випадку ви хочете залишити коментарі, щоб пояснити свій код.

// 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); }); }); }); }); };

Тепер замість того, щоб думати "wtf ?!" коли ви бачите пекло зворотного дзвінка, ви розумієте, чому це потрібно писати саме так.

Друге рішення пекла зворотного виклику: розділіть зворотні виклики на різні функції

Наш приклад пекло зворотного виклику вже є прикладом цього. Дозвольте мені показати вам покроковий імперативний код, і ви побачите, чому.

Для getBeefнашого першого зворотного дзвінка нам доведеться піти до холодильника, щоб отримати яловичину. На кухні два холодильники. Нам потрібно підійти до потрібного холодильника.

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

Щоб приготувати яловичину, нам потрібно поставити яловичину в духовку; увімкніть духовку на 200 градусів, і почекайте двадцять хвилин.

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

А тепер уявіть, якщо вам доведеться писати кожен з цих кроків у makeBurger... ви, мабуть, знепритомнієте від великої кількості коду!

Для конкретного прикладу розподілу зворотних викликів на менші функції ви можете прочитати цей невеликий розділ у моїй статті зворотного виклику.

Третє рішення пекла зворотного дзвінка: використовуйте обіцянки

Я припускаю, що ви знаєте, що таке обіцянки. Якщо ні, прочитайте цю статтю.

Обіцянки можуть значно полегшити управління зворотним дзвінком. Замість вкладеного коду, який ви бачите вище, у вас буде такий:

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));

Якщо ви скористаєтесь одним аргументом із обіцянками, ви можете налаштувати вищезазначене до цього:

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

Набагато легше читати та керувати ними.

Але питання полягає в тому, як перетворити код на основі зворотного виклику в код на основі обіцянки.

Перетворення зворотних дзвінків на обіцянки

To convert callbacks into promises, we need to create a new promise for each callback. We can resolve the promise when the callback is successful. Or we can reject the promise if the callback fails.

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 practice, callbacks would probably be written for you already. If you use Node, each function that contains a callback will have the same syntax:

  1. The callback would be the last argument
  2. The callback will always have two arguments. And these arguments are in the same order. (Error first, followed by whatever you’re interested in).
// 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 });

If your callback has the same syntax, you can use libraries like ES6 Promisify or Denodeify (de-node-ify) that callback into a promise. If you use Node v8.0 and above, you can use util.promisify.

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.