Howtojs

Callback Hell

3 февраля 2021 г. • ☕️ 4 мин.

Что такое Callback Hell?

В статье Как вернуть результат из асинхронного вызова? я приводил пример о том, как строить последовательность асинхронных вызовов с использованием callback.

В реальности, количество вложенных вызовов может вырасти до такой степени, что такой код будет сложно поддерживать.

fs.readdir(process.cwd(), { withFileTypes: true }, (err, dir) => {  if (err) throw err

  const fileNames = dir
    .filter(d => d.isFile() && path.extname(d.name) === ".js")
    .map(f => f.name)

  readFiles(fileNames, (err, files) => {    if (err) throw err

    const totalsize = files.map(f => f.length).reduce((a, b) => a + b, 0)

    request.post("https://api.com/totalsize", { totalsize }, (err, res) => {      if (err) throw err

      fs.writeFile("result.json", res.body, err => {        if (err) throw err

        console.log("Success")      })
    })
  })
})

Причина в том, что при использовании callbacks, логическая цепочка строиться не сверху вниз(как при написании кода с блокирующими вызовами), а слева направо.

Упростить такой код и избежать callback hell можно несколькими способами.

1. Разбить код на именованные функции

function reportFileSizes() {  fs.readdir(process.cwd(), { withFileTypes: true }, handleDir)
}

function handleDir(err, dir) {  if (err) throw err

  const fileNames = dir
    .filter(d => d.isFile() && path.extname(d.name) === ".js")
    .map(f => f.name)

  readFiles(fileNames, (err, files), handleDirFiles)
}

function handleDirFiles(err, files) {  if (err) throw err

  const totalsize = files.map(f => f.length).reduce((a, b) => a + b, 0)

  request.post("https://api.com/totalsize", { totalsize }, handleApiResponse)
}

function handleApiResponse(err, res) {  if (err) throw err

  fs.writeFile("result.json", res.body, handeReportFileWritten)
}

function handeReportFileWritten(err) {  if (err) throw err

  console.log("Success")
}

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

2. Использовать Promise

fs.readdir(process.cwd(), { withFileTypes: true })  .then(dir => {
    const fileNames = dir
      .filter(d => d.isFile() && path.extname(d.name) === ".js")
      .map(f => f.name)

    return readFiles(fileNames)  })
  .then(files => {
    const totalsize = files.map(f => f.length).reduce((a, b) => a + b, 0)

    return request.post("https://api.com/totalsize", { totalsize })  })
  .then(res => fs.writeFile("result.json", res.body))  .then(() => console.log("Success"))

Не смотря на то, что в коде присутствуют вызовы then и callbacks, такой код значительно легче читать сверху вниз.

3. Использовать async/await (рекомендуется)

async function main() {
  const dir = await fs.readdir(process.cwd(), { withFileTypes: true })
  const fileNames = dir
    .filter(d => d.isFile() && path.extname(d.name) === ".js")
    .map(f => f.name)

  const files = await Promise.all(fileNames.map(n => fs.readFile(n)))
  const totalsize = files.map(f => f.length).reduce((a, b) => a + b, 0)

  const res = await request.post("https://api.com/totalsize", { totalsize })
  await fs.writeFile("result.json", res.body)
  console.log("Success")
}

Такой код комибинирует лучше из двух миров и может называться псевдосинхронным. Не смотря на то, что этот код выглядит как синхронный, за счет магии async/await он является асинхронным.