logo
Ещё

Что такое callback-функция в JavaScript

И у начинающих разработчиков, и у тех, кто переходит в язык JavaScript из других ЯП, осознание и использование callback-функций обычно вызывает трудности, потому что в большинстве языков асинхронное программирование строго контролируется разработчиком, в то время как в JS асинхронный код можно создать просто по незнанию особенностей встроенной функции. Callback, механизм обратных вызовов, как раз и создан для «правильного» написания асинхронного кода, и ниже мы расскажем, как это работает.

Синхронное и асинхронное исполнение кода

Для начала опишем проблему, из-за которой функции обратного вызова вообще нужны. Если изучать какой-нибудь «серверный» язык программирования, то вопрос обработки асинхронных действий вообще не будет проблемой, пока вы сами не начнете использовать асинхронные операции – и код, и все зависимости, и результат исполнения обычно лежат в одном месте, поэтому синхронного кода более чем достаточно, а разработчики используют методы асинхронного программирования в основном для оптимального использования ресурсов железа.

С JS такой фокус не пройдет, потому что во время написания кода практически всегда нужно обращаться к сторонним ресурсам – API собственного сервера и API сторонних приложений. А API мало того, что требует некоторого времени на обработку запроса, так еще и непонятно, что эта API вернет – нужный результат или ошибку 404. Поэтому многие встроенные функции JS работают как асинхронные запросы – они выполняются не последовательно (строка за строкой кода), а тогда, когда это будет необходимо (event-driven модель языка).

Приведем 2 коротких примера. Синхронный код:

console.log('Шаг 1');

console.log('Шаг 2');

console.log('Шаг 3');

Асинхронный код:

console.log('Шаг 1');

setTimeout(() => {

console.log('Шаг 2');

}, 2000);

console.log('Шаг 3');

setTimeout – асинхронный метод, поэтому «Шаг 2» выведется после «Шаг 3» –интерпретатор исполнит метод после того, как пройдет время, указанное в setTimeout.

Итак, встроенная в JS асинхронность является жизненной необходимостью, потому что без нее скрипты на пользовательской стороне будут намертво зависать при каждом обращении скрипта к API (JavaScript – однопоточный язык). Но эта асинхронность создает проблему: мы не можем быть уверены в том, что объект, которым мы манипулируем, вообще существует. Например: у нас есть приложение, которое подтягивает информацию о пользователе с сервера и на основе этой информации модифицирует интерфейс; запрос информации работает асинхронно, модификация интерфейса работает синхронно. Учитывая, что железо клиента работает быстрее, чем интернет-соединение между клиентом и пользователем, нам придется сколько-то ждать, пока не придет информация – но сколько именно? Секунда, 5 секунд? Именно для того, чтобы решить эту проблему, используют callback-функции.

Callback – что это и зачем нужно в JS

В JavaScript функции являются одновременно функциями первого класса и высшего порядка – то есть их можно использовать в качестве объектов и передавать в другие функции. Механика callback на этом и построена: мы передаем функцию А в функцию Б для того, чтобы впоследствии вызвать функцию Б внутри функции А – это гарантирует то, что сначала исполнится функция А, а потом исполнится функция Б. Пример:

function greeting(name) {

console.log('Hello ' + name);

}

function processUserInput(callback) {

const name = prompt('Please enter your name.');

callback(name);

}

processUserInput(greeting);

Здесь мы создаем 2 функции: greeting, которая принимает в качестве аргумента имя пользователя name, и processUserInput, которая принимает в качестве аргумента функцию callback. Затем мы вызываем processUserInput, которому передаем функцию greeting. ProcessUserInput исполняется, создает константу с именем и внутри себя вызывает переданную функцию greeting, которой передает константу с именем.

Пример выше – синхронный callback, то есть функции все еще выполняются последовательно, мы просто поместили одну в другую. Это может быть полезно, если мы хотим «причесать» код или работаем с методом, который ожидает колбэк, но куда более полезны асинхронные колбэки – они решают проблему, которую мы описывали в предыдущем разделе:

function fetchData(callback) {

setTimeout(() => {

const data = { id: 1, title: 'Sample Data' };

callback(data);

}, 2000);

}

function handleData(data) {

console.log('Data received:', data);

}

fetchData(handleData);

Здесь мы имитируем асинхронную функцию через setTimeout. У нас есть fetch, и у нас есть функция, которая обрабатывает данные, пришедшие от fetch – будет затруднительно обработать данные, которые еще не пришли, верно? Поэтому мы передаем нашей функции fetchData функцию handleData в качестве аргумента, чтобы гарантированно вызвать handleData после того, как данные придут. И fetchData, и handleData будут выведены из потока исполнения до момента прихода данных, то есть нам не придется останавливать все приложение.

Примеры callback-функций

Обработка ошибок

Часто callback-функции используют для обработки ошибок. Пример:

function loadScript(src, callback) {

let script = document.createElement('script');

script.src = src;

script.onload = () => callback(null, script);

script.onerror = () => callback(new Error(`Ошибка загрузки скрипта ${src}`));

document.head.append(script);

}

function handleScriptLoad(error, script) {

if (error) {

console.error('Ошибка:', error.message);

} else {

console.log('Скрипт загружен успешно:', script.src);

}

}

// Использование функции loadScript

loadScript('https://example.com/some-script.js', handleScriptLoad);

Здесь мы создаем отдельную функцию handleScriptLoad, которая обрабатывает 2 сценария: правильная загрузка скрипта и ошибка во время загрузки скрипта. Затем мы передаем эту функцию в качестве аргумента загрузчику, который вызывает handleScriptError как при успехе (с параметрами, верифицирующими успех), так и при ошибке (с новой ошибкой). Мы можем повесить этот скрипт обработки ошибок на все загрузчики, если впоследствии мы захотим его модифицировать – это нужно будет сделать только в одном месте, в самом скрипте.

Обработка событий

Многие события в браузере обрабатываются синхронно, что логично – пользователь может нажать на кнопку, а может и не нажать. Здесь нам пригодятся синхронные колбэки – на каждое событие мы вешаем event listener, который будет исполнять действие при наступлении события:

document.getElementById('myButton').addEventListener('click', function() {

console.log('Button was clicked!');

});

Заметим, что для обработки событий часто создают анонимные callback-функции.

Таймеры

Выше мы уже приводили примеры с setTimeout – они могут попасться и на реальных проектах, частый кейс: всплывающие окна. Работает все просто:

setTimeout(function() {

console.log('This message is displayed after 2 seconds');

}, 2000);

setInterval(function() {

console.log('This message is displayed every 3 seconds');

}, 3000);

В скрипт добавляется таймер, который через некоторое время (или через определенные интервалы времени) исполнит некоторый код.

«Callback hell» и как его избежать

Посмотрите на код:

getUser(userId, function(err, user) {

if (err) {

console.error('Ошибка получения пользователя:', err);

} else {

getTasks(user.id, function(err, tasks) {

if (err) {

console.error('Ошибка получения задач:', err);

} else {

updateTaskStatus(tasks[0].id, 'completed', function(err, result) {

if (err) {

console.error('Ошибка обновления задачи:', err);

} else {

sendNotification(user.email, 'Задача обновлена', function(err, response) {

if (err) {

console.error('Ошибка отправки уведомления:', err);

} else {

console.log('Уведомление отправлено:', response);

}

});

}

});

}

});

}

});

Он работает и выполняет свои задачи, но есть проблема: его очень сложно читать. Так получилось, потому что у нас в одном модуле собрались 4 действия, которые нужно сделать последовательно – взять данные пользователя, получить его список задач, обновить статус первой задачи и выслать нотификацию. На каждом этапе нам дополнительно нужно проверить, не возникла ли ошибка – итого 8 последовательных действий, объединенных в цепочку колбэков. Это и есть callback hell – ситуация, когда мы создаем длинную сложночитаемую цепочку колбэков.

Простого решения проблемы callback hell не существует – код придется рефакторить. Вот какие рекомендации существуют:

  • Уменьшите вложенность. Дайте имена анонимным функциям и выведите их в отдельные блоки – это упростит чтение кода.
  • Разбейте функционал на модули. Отдельные функции можно не просто вывести в отдельные блоки, а вообще вынести в отдельные файлы – вы получите маленькие библиотеки, называемые модулями. Для небольшого проекта это может быть избыточно, но с ростом строк кода модульность становится жизненно необходимой.
  • Создавайте обработчики ошибок. Если обрабатывать каждую отдельную ошибку прямо в функции, то объем кода увеличится вдвое, к тому же вы повысите свою вероятность что-то упустить. Отдельный продуманный обработчик ошибок решает все эти проблемы.

Если же обычные колбэки – вообще не вариант, то есть более продвинутые инструменты языка – промисы и async/await. Обсуждение обоих инструментов выходит далеко за рамки материала, но для примера – вот как выглядит код выше, если переписать его с использованием promises:

getUser(userId)

.then(user => getTasks(user.id))

.then(tasks => updateTaskStatus(tasks[0].id, 'completed'))

.then(result => sendNotification(user.email, 'Задача обновлена'))

.then(response => console.log('Уведомление отправлено:', response))

.catch(err => console.error('Ошибка:', err));

Кратко о главном

  • В JavaScript многие стандартные функции используют асинхронность из-за частой работы с API серверов и event-driven моделью языка.
  • Callback – это механизм, позволяющий гарантировать исполнение одной функции строго после другой.
  • Часто колбэки используются для таймеров, а также обработки ошибок и пользовательских событий.
  • Callback hell – это ситуация, в которой код становится сложночитаемым из-за большого количества вложенных функций. Решается рефакторингом, промисами и async/await.