Работа с вложенными обратными вызовами | Кодементор

JavaScript — странный язык. Время от времени вам приходится иметь дело с обратным вызовом, который находится в другом обратном вызове, который находится в еще одном обратном вызове.

Люди ласково называют этот узор ад обратного звонка.

Это выглядит примерно так:

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

Это JavaScript для вас. Удивительно видеть вложенные обратные вызовы, но я не думаю, что это «ад». С «адом» можно справиться, если знать, что с ним делать.

Об обратных вызовах

Я предполагаю, что вы знаете, что такое обратные вызовы, если вы читаете эту статью. Если вы этого не сделаете, пожалуйста, прочитайте эта статья для ознакомления с обратными вызовами, прежде чем продолжить. Там мы поговорим о том, что такое обратные вызовы и почему вы используете их в JavaScript.

Решения для ада обратного вызова

Есть четыре решения ада обратного вызова:

  1. Пишите комментарии
  2. Разделите функции на более мелкие функции
  3. Использование промисов
  4. Использование асинхронного/ожидания

Прежде чем мы углубимся в решения, давайте вместе создадим ад обратного вызова. Почему? Потому что это слишком абстрактно, чтобы видеть 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)
        })
      })
    })
  })
}

Теперь, вместо того, чтобы думать «втф?!» когда вы видите ад обратного вызова, вы понимаете, почему он должен быть написан именно так.

Второе решение ада обратных вызовов: разделить обратные вызовы на разные функции.

Наш пример callback hell уже является примером этого. Позвольте мне показать вам пошаговый императивный код, и вы поймете, почему.

За 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)

Гораздо проще читать и управлять.

Но вопрос в том, как преобразовать код на основе обратного вызова в код на основе обещаний.

Преобразование обратных вызовов в обещания

Чтобы преобразовать обратные вызовы в обещания, нам нужно создать новое обещание для каждого обратного вызова. Мы можем resolve обещание при успешном обратном вызове. Или мы можем reject обещание в случае сбоя обратного вызова.

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

На практике обратные вызовы, вероятно, уже были бы написаны для вас. Если вы используете Node, каждая функция, содержащая обратный вызов, будет иметь одинаковый синтаксис:

  1. Обратный вызов будет последним аргументом
  2. Обратный вызов всегда будет иметь два аргумента. И эти аргументы в том же порядке. (Сначала ошибка, затем то, что вас интересует).
// 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 
})

Если ваш обратный вызов имеет тот же синтаксис, вы можете использовать такие библиотеки, как ES6 Обещание или же Обнажить (de-node-ify) этот обратный вызов в обещание. Если вы используете Node v8.0 и выше, вы можете использовать util.promisify.

Все трое работают. Вы можете выбрать любую библиотеку для работы. Однако между каждым методом есть небольшие нюансы. Я оставлю вас, чтобы проверить их документацию для получения инструкций.

Четвертое решение ада обратных вызовов: используйте асинхронные функции

Чтобы использовать асинхронные функции, вам нужно сначала знать две вещи:

  1. Как конвертировать обратные вызовы в промисы (читайте выше)
  2. Как использовать асинхронные функции (прочитайте это если вам нужна помощь).

С асинхронными функциями вы можете написать makeBurger как будто это снова синхронно!

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)

Есть одно улучшение, которое мы можем внести в makeBurger здесь. Вы, вероятно, можете получить двух помощников, чтобы getBuns а также getBeef в то же время. Это означает, что вы можете await их обоих с 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)

(Примечание: вы можете сделать то же самое с промисами… но синтаксис не такой приятный и понятный, как асинхронные/ожидающие функции).

Подведение итогов

Ад обратного звонка не так ужасен, как вы думаете. Есть четыре простых способа справиться с адом обратных вызовов:

  1. Пишите комментарии
  2. Разделите функции на более мелкие функции
  3. Использование промисов
  4. Использование асинхронного/ожидания

Спасибо за чтение. Эта статья изначально была размещена на мой блог. Подписаться на моя рассылка если вы хотите больше статей, которые помогут вам стать лучшим разработчиком внешнего интерфейса.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *