Что такое callback в программировании
Перейти к содержимому

Что такое callback в программировании

  • автор:

Введение: колбэки

Для демонстрации использования колбэков, промисов и других абстрактных понятий мы будем использовать некоторые браузерные методы: в частности, загрузку скриптов и выполнение простых манипуляций с документом.

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

Тем не менее, мы все равно попытаемся максимально доходчиво всё разъяснить. Ничего особо сложного в плане браузера не будет.

Многие действия в JavaScript асинхронные.

Например, рассмотрим функцию loadScript(src) :

function loadScript(src)

Эта функция загружает на страницу новый скрипт. Когда в тело документа добавится конструкция , браузер загрузит скрипт и выполнит его.

Вот пример использования этой функции:

// загрузит и выполнит скрипт loadScript('/my/script.js');

Такие функции называют «асинхронными», потому что действие (загрузка скрипта) будет завершено не сейчас, а потом.

Если после вызова loadScript(…) есть какой-то код, то он не будет ждать, пока скрипт загрузится.

loadScript('/my/script.js'); // код, написанный после вызова функции loadScript, // не будет дожидаться полной загрузки скрипта // . 

Мы хотели бы использовать новый скрипт, как только он будет загружен. Скажем, он объявляет новую функцию, которую мы хотим выполнить.

Но если мы просто вызовем эту функцию после loadScript(…) , у нас ничего не выйдет:

loadScript('/my/script.js'); // в скрипте есть "function newFunction() " newFunction(); // такой функции не существует!

Действительно, ведь у браузера не было времени загрузить скрипт. Сейчас функция loadScript никак не позволяет отследить момент загрузки. Скрипт загружается, а потом выполняется. Но нам нужно точно знать, когда это произойдёт, чтобы использовать функции и переменные из этого скрипта.

Давайте передадим функцию callback вторым аргументом в loadScript , чтобы вызвать её, когда скрипт загрузится:

function loadScript(src, callback) < let script = document.createElement('script'); script.src = src; script.onload = () =>callback(script); document.head.append(script); >

Событие onload описано в статье Загрузка ресурсов: onload и onerror, оно в основном выполняет функцию после загрузки и выполнения скрипта.

Теперь, если мы хотим вызвать функцию из скрипта, нужно делать это в колбэке:

loadScript('/my/script.js', function() < // эта функция вызовется после того, как загрузится скрипт newFunction(); // теперь всё работает . >);

Смысл такой: вторым аргументом передаётся функция (обычно анонимная), которая выполняется по завершении действия.

Возьмём для примера реальный скрипт с библиотекой функций:

function loadScript(src, callback) < let script = document.createElement('script'); script.src = src; script.onload = () =>callback(script); document.head.append(script); > loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => < alert(`Здорово, скрипт $загрузился`); alert( _ ); // функция, объявленная в загруженном скрипте >);

Такое написание называют асинхронным программированием с использованием колбэков. В функции, которые выполняют какие-либо асинхронные операции, передаётся аргумент callback — функция, которая будет вызвана по завершению асинхронного действия.

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

Колбэк в колбэке

Как нам загрузить два скрипта один за другим: сначала первый, а за ним второй?

Первое, что приходит в голову, вызвать loadScript ещё раз уже внутри колбэка, вот так:

loadScript('/my/script.js', function(script) < alert(`Здорово, скрипт $загрузился, загрузим ещё один`); loadScript('/my/script2.js', function(script) < alert(`Здорово, второй скрипт загрузился`); >); >);

Когда внешняя функция loadScript выполнится, вызовется та, что внутри колбэка.

А что если нам нужно загрузить ещё один скрипт.

loadScript('/my/script.js', function(script) < loadScript('/my/script2.js', function(script) < loadScript('/my/script3.js', function(script) < // . и так далее, пока все скрипты не будут загружены >); >) >);

Каждое новое действие мы вынуждены вызывать внутри колбэка. Этот вариант подойдёт, когда у нас одно-два действия, но для большего количества уже не удобно. Альтернативные подходы мы скоро разберём.

Перехват ошибок

В примерах выше мы не думали об ошибках. А что если загрузить скрипт не удалось? Колбэк должен уметь реагировать на возможные проблемы.

Ниже улучшенная версия loadScript , которая умеет отслеживать ошибки загрузки:

function loadScript(src, callback) < let script = document.createElement('script'); script.src = src; script.onload = () =>callback(null, script); script.onerror = () => callback(new Error(`Не удалось загрузить скрипт $`)); document.head.append(script); >

Мы вызываем callback(null, script) в случае успешной загрузки и callback(error) , если загрузить скрипт не удалось.

loadScript('/my/script.js', function(error, script) < if (error) < // обрабатываем ошибку >else < // скрипт успешно загружен >>);

Опять же, подход, который мы использовали в loadScript , также распространён и называется «колбэк с первым аргументом-ошибкой» («error-first callback»).

  1. Первый аргумент функции callback зарезервирован для ошибки. В этом случае вызов выглядит вот так: callback(err) .
  2. Второй и последующие аргументы — для результатов выполнения. В этом случае вызов выглядит вот так: callback(null, result1, result2…) .

Одна и та же функция callback используется и для информирования об ошибке, и для передачи результатов.

Адская пирамида вызовов

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

Но для нескольких асинхронных действий, которые нужно выполнить друг за другом, код выглядит вот так:

loadScript('1.js', function(error, script) < if (error) < handleError(error); >else < // . loadScript('2.js', function(error, script) < if (error) < handleError(error); >else < // . loadScript('3.js', function(error, script) < if (error) < handleError(error); >else < // . и так далее, пока все скрипты не будут загружены (*) >>); > >) > >);
  1. Мы загружаем 1.js . Продолжаем, если нет ошибок.
  2. Мы загружаем 2.js . Продолжаем, если нет ошибок.
  3. Мы загружаем 3.js . Продолжаем, если нет ошибок. И так далее (*) .

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

Иногда это называют «адом колбэков» или «адской пирамидой колбэков».

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

Такой подход к написанию кода не приветствуется.

Мы можем попытаться решить эту проблему, изолируя каждое действие в отдельную функцию, вот так:

loadScript('1.js', step1); function step1(error, script) < if (error) < handleError(error); >else < // . loadScript('2.js', step2); >> function step2(error, script) < if (error) < handleError(error); >else < // . loadScript('3.js', step3); >> function step3(error, script) < if (error) < handleError(error); >else < // . и так далее, пока все скрипты не будут загружены (*) >>;

Заметили? Этот код делает всё то же самое, но вложенность отсутствует, потому что все действия вынесены в отдельные функции.

Код абсолютно рабочий, но кажется разорванным на куски. Его трудно читать, вы наверняка заметили это. Приходится прыгать глазами между кусками кода, когда пытаешься его прочесть. Это неудобно, особенно, если читатель не знаком с кодом и не знает, что за чем следует.

Кроме того, все функции step* одноразовые, и созданы лишь только, чтобы избавиться от «адской пирамиды вызовов». Никто не будет их переиспользовать где-либо ещё. Таким образом, мы, кроме всего прочего, засоряем пространство имён.

Нужно найти способ получше.

К счастью, такие способы существуют. Один из лучших — использовать промисы, о которых рассказано в следующей главе.

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

Что такое callback-функция в JavaScript? главное изображение

Простыми словами: коллбэк — это функция, которая должна быть выполнена после того, как другая функция завершила выполнение (отсюда и название: callback — функция обратного вызова).

Чуть сложнее: В JavaScript функции — это объекты. Поэтому функции могут принимать другие функции в качестве аргументов, а также возвращать функции в качестве результата. Функции, которые это умеют, называются функциями высшего порядка. А любая функция, которая передается как аргумент, называется callback-функцией.

Зачем нужны коллбэки?

По одной простой причине: JavaScript — это событийно-ориентированный язык. Поэтому вместо того, чтобы ждать ответа для дальнейшего выполнения программы, JavaScript продолжит выполнение, одновременно ожидая других событий. Давайте разберем простой пример:

const first = () =>  console.log(1); >; const second = () =>  console.log(2); >; first(); second(); 

Как вы и ожидаете, функция first выполнится первой, а функция second уже после нее. Поэтому в консоли будет выведен следующий результат:

Пока что все понятно. Но что, если функция first содержит некий код, который не может выполниться немедленно? К примеру, работа с API, где мы отправляем запрос и должны ждать ответа. Чтобы смоделировать такую ситуацию, мы используем функцию setTimeout , которая вызывает функцию после заданного временного промежутка. Мы отсрочим выполнение функции на 500 миллисекунд, как будто бы это запрос к некому API. Теперь код будет выглядеть так:

const first = () =>  // Как будто бы запрос к API setTimeout(() =>  console.log(1); >, 500 ); >; const second = () =>  console.log(2); >; first(); second(); 

Неважно, понимаете ли вы сейчас, как работает setTimeout() . Основная идея — теперь мы отложили исполнение команды console.log(1) на 500 миллисекунд. И что теперь выведет наша программа?

Хотя мы по-прежнему вызываем функцию first первой, ее вывод появился вторым, после вывода функции second . Но JavaScript не нарушает порядок вызова функций, он просто не дожидается ответа от функции first , а сразу двигается дальше — к функции second .

Поэтому нельзя просто вызывать функции в нужном порядке и надеяться, что они в любом случае выполнятся в том же порядке. Коллбэки же позволяют нам быть уверенными в том, что определенный код не начнет исполнение до того момента, пока другой код не завершит исполнение.

Фронтенд-разработчик — с нуля до трудоустройства за 10 месяцев

  • Постоянная поддержка от наставника и учебного центра
  • Помощь с трудоустройством
  • Готовое портфолио к концу обучения
  • Практика с первого урока

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

Создаем коллбэк

Во-первых, откройте консоль разработчика в Google Chrome (Windows: Ctrl + Shift + J)(Mac: Cmd + Option + J), либо свой IDE, либо просто Repl.it , и введите в консоли следующую функцию:

const doHomework = (subject) =>  alert(`Starting my $subject> homework.`); >; 

Мы создали функцию doHomework . Наша функция принимает одну переменную — название предмета, которым мы будем заниматься. Вызовите функцию, набрав следующий текст в консоли:

doHomework('math'); // Выводит алерт: Starting my math homework. 

Теперь давайте добавим в определение функции еще один параметр, это и будет наш коллбэк. Затем вызовем ее, определив функцию-callback в качестве аргумента:

const doHomework = (subject, callback) =>  alert(`Starting my $subject> homework.`); callback(); >; doHomework('math', () =>  alert('Finished my homework'); >); 

Если вы введете этот код в консоли, вы получите два алерта один за другим, в первом будет сообщение о том, что выполнение домашнего задания началось (Starting my math homework.), а во втором — что вы закончили выполнять задание (Finished my homework).

Однако коллбэки не обязательно должны быть определены при вызове функции. Они могут быть определены и в другом месте кода, например, так:

const doHomework = (subject, callback) =>  alert(`Starting my $subject> homework.`); callback(); >; const alertFinished = () =>  alert('Finished my homework'); >; doHomework('math', alertFinished); 

Таким образом, результат выполнения этого кода такой же, как и в предыдущем примере, однако сам код немного другой. Как вы видите, мы передали функцию alertFinished как аргумент в функцию doHomework при ее вызове.

Перепишем пример вызова функции с setTimeout для последовательного выполнения функций:

const first = (callback) =>  // Как будто бы запрос к API setTimeout(() =>  console.log(1); callback(); >, 500 ); >; const second = () =>  console.log(2); >; first(second); // 1 // 2 

Пример из реальной жизни

На прошлой неделе я опубликовал статью «Создаем бота для Твиттера в 38 строк кода» . Этот код работает благодаря API Твиттера. И когда мы делаем запрос к API, мы должны дождаться ответа до того, как начнем выполнять с этим ответом какие-то действия. Это прекрасный пример того, как в реальной жизни выглядит коллбэк. Вот как выглядит сам запрос:

T.get('search/tweets', params, (err, data, response) =>  if (!err)  // Происходит какая-то магия > else  console.log(err); > >); 

T.get просто значит, что мы выполняем get запрос к API Твиттера. В запросе три параметра: ‘search/tweets’ – это адрес (роут) запроса, params – наши параметры поиска и в конце передается анонимная функция-callback.

Коллбэк здесь нужен, потому что нам нужно дождаться ответа от сервера до того, как приступим к дальнейшему выполнению кода. Мы не знаем, успешным будет наш запрос или нет, поэтому после отправки параметров поиска на search/tweets через get-запрос, мы просто ждем. Как только Твиттер ответит, выполнится наша callback-функция. Твиттер отправит нам в качестве ответа или объект err (error – ошибка), или объект response. В коллбэке мы можем через if() проверить, был ли запрос успешным или нет, и затем действовать соответственно.

Профессия «Фронтенд-разработчик»

  • Изучите востребованные JavaScript и TypeScript
  • Научитесь создавать пользовательские интерфейсы сайтов и приложений
  • Освойте самый популярный фреймворк JavaScript — React
  • Познакомьтесь с языками веб-разработки HTML и CSS

Callback (программирование)

Callback (англ. call — вызов, англ. back — обратный) или фу́нкция обра́тного вы́зова в программировании — передача исполняемого кода в качестве одного из параметров другого кода. Обратный вызов позволяет в функции исполнять код, который задаётся в аргументах при её вызове. Этот код может быть определён в других контекстах программного кода и быть недоступным для прямого вызова из этой функции. Некоторые алгоритмические задачи в качестве своих входных данных имеют не только числа или объекты, но и действия (алгоритмы), которые естественным образом задаются как обратные вызовы.

Применение

Концепция обратного вызова имеет много приложений. Например, некоторые алгоритмы (функции) в качестве подзадачи имеют задачу вычисления хеш-значения от строки. Какую именно функцию использовать для вычисления хеш-значений удобно задавать в аргументах при запуске алгоритма (функции).

Другой пример алгоритма, которому естественно передавать в аргументе функцию, — алгоритм обхода какого-либо хранилища объектов с применением некоторого действия к каждому объекту. Обратный вызов может выступать в роли этого действия (алгоритма).

Техника программирования обратного вызова в языках программирования, подобных языку Си, проста. При вызове основной функции ей просто передаётся указатель на функцию обратного вызова. Классическим примером является функция qsort из библиотеки stdlib. Эта функция позволяет отсортировать массив блоков байт одинаковой длины. В качестве аргументов она получает адрес первого элемента массива, количество блоков в массиве, размер блока байт, и указатель на функцию сравнения двух блоков байт. Эта функция сравнения и есть функция обратного вызова в данном примере:

#include // функция сравнения целых чисел по модулю int compare_abs(const void *a, const void *b)  int a1 = *(int*)a; int b1 = *(int*)b; return abs(a1) - abs(b1); > int main()  int m[10] = 1,-3,5,-100,7,33,44,67,-4, 0>; // сортировка массива m по возрастанию модулей qsort(m, 10, sizeof(int), compare_abs); return 0; > 

Об обратном вызове можно думать как о действии, передаваемом некоторой основной процедуре в качестве аргумента. И это действие может рассматриваться как:

  • подзадача и использоваться для обработки данных внутри этой процедуры;
  • «телефонная связь», используемая для того, чтобы «связываться» с тем, кто вызвал процедуру, при наступлении какого-то события (англ.callback дословно переводится как «звонок обратно»).

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

#include #include volatile sig_atomic_t br = 1; void sig(int signum)  br=0; > int main(int argc, char *argv[])  signal(SIGINT, sig); printf("Press break keyboard key combination to stop the program\n"); while(br); printf("Received SIGINT, exit\n"); return 0; > 

В некоторых языках программирования, таких как Common Lisp, Scheme, Clojure, Javascript, Perl, Python, Ruby и других, есть возможность конструировать анонимные (не именованные) функции и функции-замыкания прямо в выражении вызова основной функции, и эта возможность широко используется.

В технологии AJAX при выполнении асинхронного запроса к серверу необходимо указывать функцию обратного вызова, которая будет вызвана, как только придёт ответ на запрос. Часто эту функцию определяют «прямо на месте», не давая ей никакого определённого имени:

new Ajax.Request('http://example.com/do_it',  method: 'post', onSuccess: function(transport)  // функция, вызываемая window.alert("Done!"); // при успешном выполнении запроса >, // onFailure: function()  // функция, вызываемая window.alert("Error!"); // при ошибке выполнения запроса > >); 

Функция обратного вызова используется также в шаблоне проектирования «Наблюдатель» (Observer). Так, например, используя библиотеку Prototype, можно создать «наблюдателя», который следит за нажатиями на элемент с идентификатором «my_button» и при получении события пишет сообщение внутрь элемента «message_box» :

Event.observe ($("my_button"), 'click', function()  $("message_box").innerHTML = "Вы нажали на кнопку!" >); 

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

Зачем использовать функции обратного вызова

Для лучшего понимания причин использования обратного вызова рассмотрим простую задачу выполнения следующих операций над списком чисел: напечатать все числа, возвести все числа в квадрат, увеличить все числа на 1, обнулить все элементы. Ясно, что алгоритмы выполнения этих четырёх операций схожи — это цикл обхода всех элементов списка с некоторым действием в теле цикла, применяемый к каждому элементу. Это несложный код, и в принципе можно написать его 4 раза. Но давайте рассмотрим более сложный случай, когда список у нас хранится не в памяти, а на диске, и со списком могут работать несколько процессов одновременно и необходимо решать проблемы синхронизации доступа к элементам (несколько процессов могут выполнять разные задачи — удаления некоторых элементов из списка, добавления новых, изменение существующих элементов в списке). В этом случае задача обхода всех элементов списка будет довольно сложным кодом, который не хотелось бы копировать несколько раз. Правильнее создать функцию общего назначения для обхода элементов списка и дать возможность программистам абстрагироваться от того, как именно устроен алгоритм обхода и писать лишь функцию обратного вызова для обработки отдельного элемента списка.

Структурирование ПО

Структурирование программного обеспечения через функции обратного вызова — очень удобный и широко используемый подход, так как при этом поведение программы с неизменным (в том числе закрытым) кодом можно изменять в очень широких пределах. Это реализуется двумя путями — или «альтернативной реализацией» какой-либо функции, или «добавлением в цепочку вызовов» ещё одной функции.

Как правило, разработчик реализует через обратные вызовы не всю функциональность программы, а лишь ту, которую предполагается расширять или изменять плагинами. Для подключения плагинов предоставляется специальная процедура, которая и заменяет «стандартные» обратные функции от разработчика на альтернативные из плагина.

Самым известным примером такого подхода является операционная система Microsoft Windows, где функции обратного вызова именуются «handler» («обработчик»), и существует возможность вставить дополнительную процедуру между любыми двумя стандартными. Этот подход называется «перехват событий» и используется, например: антивирусами для проверки файлов, к которым производится обращение; вирусами для считывания вводимых с клавиатуры символов; сетевыми фильтрами для сбора статистики и блокирования пакетов.

В современных Unix и Linux системах существует возможность динамической загрузки и выгрузки модулей ядра, работа которых также основана на функциях обратного вызова. При этом существует модуль (расширение ядра) FUSE, который, в свою очередь, предоставляет возможность обычным пользовательским программам обслуживать (перехватывать) запросы к виртуальным файловым системам.

В программном обеспечении иногда встречается декомпозиция программ, полностью основанная на функциях обратного вызова, что несколько ухудшает читаемость кода, но даёт максимальные возможности для плагинов. Пример такого продукта — DokuWiki.

Достоинства и недостатки

  • Возможность динамического изменения функциональности (подключения и отключения плагинов/модулей при работе программы).
  • Возможность неограниченного количества вариантов вызываемой функции без изменения базового (в данном контексте) кода.
  • Возможность вставки вызываемой функции не только для альтернативного поведения, но и в качестве ещё одной (промежуточной) подпрограммы — обычно для отслеживания операций или изменения параметров для следующей (вызываемой) функции. Таких независимых «дополнительных звеньев» в цепочке вызовов может быть сколько угодно.
  • Поддержка функций обратного вызова в большинстве современных языков программирования общего назначения.
  • Уменьшение производительности, связанной с дополнительными вызовами «обратной функции» — прямо пропорционально «стоимости вызова функции» в среде выполнения и количеству дополнительных вызовов при работе программы.
  • Ухудшение читаемости исходного кода — для понимания алгоритма программы необходимо отслеживать всю цепочку вызовов.

См. также

  • Сигналы (программирование)
  • Binding (программирование)
  • Алгоритм обхода двоичного дерева
  • Функция высшего порядка
  • libsigc++, библиотека обратных вызовов для языка Си++
  • libevent

Ссылки

  • callback в Win32 (рус.)
  • Style Case Study #2: Generic Callbacks (англ.)
  • C++ Callback Solution (англ.)
  • Basic Instincts: Implementing Callback Notifications Using Delegates (англ.)
  • Implement Script Callback Framework in ASP.NET (англ.)
  • Реализация функций обратного вызова в Java (англ.)
  • Реализация функций высшего порядка в С# (рус.)
  • Проверить достоверность указанной в статье информации.
  • Проставив сноски, внести более точные указания на источники.
  • Концепции языков программирования
  • Управление потоком

Wikimedia Foundation . 2010 .

Колбэк-функция

Колбэк-функция (или обратный вызов) — это функция, переданная в другую функцию в качестве аргумента, которая затем вызывается по завершению какого-либо действия.

Вот краткий пример:

function greeting(name)  alert("Hello " + name); > function processUserInput(callback)  var name = prompt("Please enter your name."); callback(name); > processUserInput(greeting); 

Выше приведён пример синхронного колбэка, поскольку функция processUserInput выполняется синхронно.

Колбэки часто используются для продолжения выполнения кода после завершения асинхронной операции — они называются асинхронными колбэками.

async function pageLoader(callback)  const data = await fetch("/ru/docs/Glossary/Callback_function"); callback(data); > function onPageLoadingFinished(pageData)  console.log("Page was sucessfully loaded!"); console.log("Response:"); console.log(pageData); > pageLoader(onPageLoadingFinished); 

Вот ещё один пример асинхронного обратного вызова: maps-example.html (живой пример). Он использует Google Maps API и Geolocation API для отображения карты текущего местоположения вашего устройства.

// maps-example.html // Вызов асинхронной функции getCurrentPosition // с передачей callback функции принимающей координаты // в качестве параметра navigator.geolocation.getCurrentPosition(function (position)  var latlng = new google.maps.LatLng( position.coords.latitude, position.coords.longitude, ); var myOptions =  zoom: 8, center: latlng, mapTypeId: google.maps.MapTypeId.TERRAIN, disableDefaultUI: true, >; var map = new google.maps.Map( document.getElementById("map_canvas"), myOptions, ); >); 

Поскольку получение координат устройства из его GPS является асинхронным (мы точно не знаем, когда данные будут возвращены), метод Geolocation.getCurrentPosition() принимает анонимную колбэк-функцию в качестве параметра, которая получает найденные данные координат. Эта функция выполняется только по возвращению данных координат.

Больше информации

Основное

  • Колбэки и события на компонентах
  • Ад обратных вызовов

Found a content problem with this page?

  • Edit the page on GitHub.
  • Report the content issue.
  • View the source on GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *