При изучении программирования мы привыкаем мыслить последовательно: строки кода выполняются по порядку. Для многих языков это утверждение верно на 100%, но всё начинает меняться, когда речь заходит про асинхронное программирование.
Это статья о том, как работают и зачем нужны колбэк-функции в JavaScript. Это одна из важнейших тем программирования на JavaScript, и ни одна более-менее серьёзная программа не обойдется без применения колбэк-функций.
У этой статьи есть продолжение — там мы рассказываем про работу с сервером и передачу параметров в коллбэк-функции
Что такое колбэк-функция
Колбэк-функция или функция обратного вызова — функция, предназначенная для отложенного выполнения. Проще говоря, она должна быть выполнена после завершения работы другой функции. Чтобы стало понятнее, разберём пример с заказом пиццы.
Колбэк (callback) переводится как «Перезвоните». Действительно, принцип работы колбэков схож с заказом обратного телефонного звонка. Представьте, что вы звоните оператору для заказа пиццы, но срабатывает автоответчик, где приятный голос просит оставаться на линии, пока не освободится оператор, или предлагает заказать обратный звонок. Когда оператор освободится — он перезвонит и примет заказ.
Это прекрасная аналогия для понимания принципов работы колбэков и асинхронности. Вместо ожидания ответа оператора, мы можем заказать обратный звонок и заниматься другими делами. Как только произойдёт колбэк (нам перезвонили), мы сможем выполнить задуманное — заказать пиццу.
Как писать код для колбэков
Посмотрим на колбэки с практической стороны.
Выше мы сказали, что колбэки неразрывно связаны с асинхронностью и позволяют «запланировать» действие, которое будет совершено после выполнения какого-то другого, возможно длительного действия. Пример с заказом пиццы это прекрасно илюстрирует. Давайте посмотрим, как это может выглядеть в коде, но для начала взглянем на синхронный код:
// Приготовление пиццы. Длительный процесс
const newPizza = makePizza('pepperoni');
// Читаем книгу пока готовят пиццу
readBook();
// Съедаем пиццу
eatPizza(newPizza);
Что в этом коде больше всего бросается в глаза? Правильно — последовательность. Здесь представлен синхронный код, который будет выполнятся последовательно:
- Мы ждём, пока для нас приготовят пиццу «Пепперони».
- Затем мы читаем книгу.
- Наконец-таки откладываем книгу в сторону и ужинаем пиццей.
Проблема видна невооруженным глазом — пока готовится пицца, мы вынуждены ждать и ничего не делать. Строка readBook()
будет выполнена только после приготовления пиццы. Фактически мы начнём читать книгу после приготовления пиццы, а не во время готовки.
Само собой, в реальном мире вместо выпекания пиццы может быть любой долгий процесс, например, запрос на получение данных с сервера.
Такой запрос не выполняется мгновенно: браузеру понадобится время, чтобы найти IP-адрес сервера, установить соединение, передать запрос, дождаться ответа и т. д. Все эти действия занимают разное количество времени. Временные задержки будут постоянно отличаться и зависеть от скорости соединения с сетью, времени выполнения запроса на сервере и некоторых других факторов.
Синхронные запросы к серверу будут блокировать дальнейшее выполнение веб-приложения, и это уже очень плохо. Представьте, что каждый раз при отправке запроса к серверу интерфейс вашего приложения становится полностью недоступным.
Эту проблему решает асинхронность, и длительные операции лучше выполнять именно асинхронно. В этом варианте мы как бы откладываем длительную операцию «на потом» и вместо ожидания завершения выполняем другой код. В этой схеме прозрачно всё, кроме вопроса: «Как выполнить код после завершения асинхронной операции?». Ответ прост — функции обратного вызова.
В JavaScript функции являются объектами высшего порядка. Это означает, что функции можно передавать в другие функции в виде параметров или возвращать в виде результата выполнения.
Рассмотрим пример:
const foo = function () {
return 'Hello, world!';
}
// Вызываем функцию и выводим результат в консоль
console.log(foo()); // Hello, world
// Выводим функцию в консоль без вызова
console.log(foo); // ƒ () { return 'Hello, world!'; }
В первом случае мы вызываем функцию foo
при помощи круглых скобок и выводим результат выполнения в консоль. Во втором примере мы не делаем вызов функции (обратите внимание на отсутствие круглых скобок), и в консоль выводится содержимое функции. Выходит, нам ничего не мешает передать функцию в виде параметра для других функций:
const runIt = function (fn) {
return fn(); // Вызываем функцию, переданную в качестве параметра
}
console.log(runIt(foo)); // Hello, world
Мы передали функцию foo
в виде параметра и вызывали её внутри функции runIt
. Вызов функции мы сделали стандартным образом — применяя круглые скобки.
Что в итоге? Мы передали ссылку на функцию в виде параметра и вызвали её внутри другой функции. В этом и заключается идея колбэков: мы передаем в виде параметров функции, которые будут вызваны «когда-нибудь потом».
И снова пицца
Вернёмся к примеру с приготовлением пиццы. Попробуем поэкспериментировать с кодом и перевести его на асинхронные рельсы. Напомню, наша задача — попросить приготовить пиццу, и читать книгу, пока пицца не будет готова.
const makePizza = function (title, cb) {
console.log(`Заказ на приготовление пиццы «${title}» получен. Начинаем готовить…`);
setTimeout(cb, 3000);
}
const readBook = function () {
console.log('Читаю книгу «Колдун и кристалл»…');
}
const eatPizza = function () {
console.log('Ура! Пицца готова, пора подкрепиться.');
}
makePizza('Пеперонни', eatPizza);
readBook();
Это рабочий код, попробуйте выполнить его в консоли и посмотреть на результат вывода. Он будет таким:
Заказ на приготовление пиццы «Пепперони» получен. Начинаем готовить…
Читаю книгу «Колдун и кристалл»…
// Здесь будет пауза
Ура! Пицца готова, пора подкрепиться.
Функция makePizza
выполняется мгновенно, и сразу за ней последовал вызов readBook
. Пока мы читали книгу, приготовилась пицца, и произошёл вызов функции eatPizza
из функции makePizza
. Задержку выполнения мы сделали искусственно с помощью setTimeout
.
Как видите, ничего сверхъестественного в колбэках нет. Это обычная функция, которая будет выполнена не сейчас, а когда-нибудь потом. «Когда-нибудь» — не преувеличение. Мы не можем сказать, в какой момент времени это случится, но можем сказать, после какой именно функции — после выполнения функции приготовлении пиццы.