В предыдущей части разбирались, как JavaScript обрабатывает асинхронный код с помощью цикла событий. Далее рассмотрим способ обеспечения правильной обработки асинхронного кода в цикле событий: функции обратного вызова – callback.
Функции обратного вызова (callback functions)
В примере setTimeout
функция с тайм-аутом запускалась после всего в основном контексте выполнения верхнего уровня. Но если понадобится, чтобы одна из функций, например third()
, выполнялась после таймаута, то придётся использовать методы асинхронного кодирования. Таймаут здесь может имитировать асинхронный вызов API, который содержит данные. Чтобы работать с данными которые вернул API, надо сперва убедится, что данные возвращаются.
Первоначальное решение этой проблемы – использование функций обратного вызова (callback functions). Функции обратного вызова (callback) не имеют специального синтаксиса; это просто функция, переданная в качестве аргумента другой функции. Функция, которая принимает другую функцию в качестве аргумента, называется функцией высшего порядка. Согласно этому определению, любая функция может стать функцией обратного вызова, если она передана в качестве аргумента. Обратные вызовы не являются асинхронными по своей природе, но могут использоваться для асинхронных целей. Вот пример синтаксического кода функции высшего порядка и обратного вызова:
// Функция
function fn() {
console.log('Просто функция')
}
// Функция, которая в качестве аргумента принимает другую функцию
function higherOrderFunction(callback) {
// Когда вы вызываете функцию,
// которая передается в качестве аргумента,
// она называется обратным вызовом (callback).
callback()
}
// Передача функции
higherOrderFunction(fn)
В этом коде определены две функции fn
и upperOrderFunction
. При вызове функции upperOrderFunction
, ей аргументом передаётся fn
и используется качестве обратного вызова. Выполнение этого кода оставит запись в консоли:
Просто функция
Но вернёмся к примеру с функциями first()
, second()
, third()
и setTimeout()
:
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}
Задача состоит в том, чтобы выполнение функции third()
откладывалось до завершения асинхронного действия в функции second()
. Вот здесь и нужны обратные вызовы. Вместо вызова third()
на верхнем уровне выполнения надо передать ее в качестве аргумента в функцию second()
. Функция second()
должна выполнить переданный ей обратный вызов по завершении асинхронного действия. Вот три функции с примененным обратным вызовом:
// Определяем три функции
function first() {
console.log(1)
}
function second(callback) {
setTimeout(() => {
console.log(2)
// Здесь выполняется обратный вызов
callback()
}, 0)
}
function third() {
console.log(3)
}
Теперь надо выполнить first()
и second()
, которой в качестве аргумента передать third()
:
first()
second(third)
После запуска этого блока кода в консоли получится:
1
2
3
Сначала будет напечатано 1
, после завершения таймера (в данном случае 0 секунд, но можно изменить его на любое значение) он напечатает 2
, затем 3
. Передача функции в качестве обратного вызова, откладывает ее выполнение до завершения асинхронного веб-API (setTimeout()
).
Основной вывод здесь заключается в том, что функции обратного вызова (callback) сами не являются асинхронными, но setTimeout
– это асинхронный веб-API, отвечающий за обработку асинхронных задач. Обратный вызов позволяет получать информацию о завершении асинхронной задачи и обрабатывать её успешный или неудачный результат.
Теперь, когда разобрались, как использовать обратные вызовы для обработки асинхронных задач, в следующем разделе рассмотрим проблемы вложенности слишком большого количества обратных вызовов.
Вложенные обратные вызовы (Nested callbacks)
Функции обратного вызова – это эффективный способ отложить выполнение функции до тех пор, пока другая функция завершится и вернёт данные. Однако из-за вложенности обратных вызовов код может очень быстро запутаться, если в нём есть много последовательных асинхронных запросов, которые зависят друг от друга. Поначалу это было большим разочарованием для JavaScript-разработчиков, и в результате код, содержащий вложенные обратные вызовы, часто называют «pyramid of doom» или «callback hell». Вот пример вложенных обратных вызовов:
function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}
Здесь каждый новый setTimeout()
вложен в функцию более высокого порядка, реализуя пирамиду обратных вызовов вглубь. Запуск этого кода вернёт следующее:
1
2
3
На практике, с использованием реального асинхронного кода, это может быть намного сложнее. Часто требуется выполнять обработку ошибок в асинхронном коде, а затем передавать некоторые данные из каждого ответа в следующий запрос. Выполнение этого с помощью обратных вызовов затрудняет чтение и поддержку кода.
Вот работающий пример более реалистичной «pyramid of doom», с которой можно поиграть здесь:
// Пример асинхронной функции
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// добавление случайного числа,
// чтобы создать впечатление асинхронной функции
// возвращает разные данные
() => callback(null, { body: args + ' ' + Math.floor(Math.random() * 10) }),
500
)
}
}
// вложенные асинхронные запросы
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// запуск
callbackHell()
В этом коде сделано так, чтобы каждая функция учитывала возможный ответ (response
) и возможную ошибку (error
), это делает функцию callbackHell
визуально запутанной. Запуск этого кода вернет что-то такое:
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (:4:21)
at second (:29:7)
at :9:13
Понятно, что этот способ обработки асинхронного кода использовать непросто. В результате в ES6 была введена концепция обещаний (promises). Этому посвящена следующая часть про обещания – promises.