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
он является асинхронным.