Как объяснить обратные вызовы на простом английском языке? Чем они отличаются от вызова одной функции из другой функции? - PullRequest
330 голосов
/ 07 марта 2012

Как объяснить обратные вызовы на простом английском языке? Чем они отличаются от вызова одной функции из другой, принимая некоторый контекст из вызывающей функции? Как объяснить их силу начинающему программисту?

Ответы [ 32 ]

508 голосов
/ 11 марта 2012

Я постараюсь, чтобы это было просто. «Обратный вызов» - это любая функция, которая вызывается другой функцией, которая принимает первую функцию в качестве параметра. В большинстве случаев «обратный вызов» - это функция, которая вызывается, когда происходит что-то . Это что-то может быть названо "событием" на языке программиста.

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

Вы могли бы сказать им, чтобы получить пакет и принести его соседям. Если ваш супруг был настолько же глуп, как компьютер, они сидели бы у двери и ждали посылку, пока она не пришла (НЕ ДЕЛАТЬ НИЧЕГО), а затем, как только она пришла, они передали бы ее соседям. Но есть и лучший способ. Скажите вашему супругу, что РАЗ, когда они получат посылку, они должны принести ее соседям. Затем они могут жить нормально, пока не получат посылку.

В нашем примере получение пакета является «событием», а передача его соседям - «обратным вызовом». Ваш супруг "выполняет" ваши инструкции, чтобы доставить посылку только , когда посылка прибудет. Намного лучше!

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

fileObject = open(file)
# now that we have WAITED for the file to open, we can write to it
fileObject.write("We are writing to the file.")
# now we can continue doing the other, totally unrelated things our program does

Здесь мы ЖДЕМ открытия файла, прежде чем писать в него. Это «блокирует» поток выполнения, и наша программа не может делать ничего другого, что может понадобиться! Что если бы мы могли сделать это вместо этого:

# we pass writeToFile (A CALLBACK FUNCTION!) to the open function
fileObject = open(file, writeToFile)
# execution continues flowing -- we don't wait for the file to be opened
# ONCE the file is opened we write to it, but while we wait WE CAN DO OTHER THINGS!

Оказывается, мы делаем это с некоторыми языками и фреймворками. Это довольно круто! Проверьте Node.js , чтобы получить некоторую реальную практику с таким типом мышления.

112 голосов
/ 10 марта 2012

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

В javascript, пример ниже.Здесь мы используем аргумент метода в качестве переменной, где мы храним информацию о функции.

function processArray(arr, callback) {
    var resultArr = new Array(); 
    for (var i = arr.length-1; i >= 0; i--)
        resultArr[i] = callback(arr[i]);
    return resultArr;
}

var arr = [1, 2, 3, 4];
var arrReturned = processArray(arr, function(arg) {return arg * -1;});
// arrReturned would be [-1, -2, -3, -4]
81 голосов
/ 12 марта 2012

Как объяснить обратные вызовы на простом английском языке?

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

Чем они отличаются от вызова одной функции из другой функции взять некоторый контекст из вызывающей функции?

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

Как объяснить их силу начинающему программисту?

Мощь обратных вызовов легко увидеть на веб-сайтах в стиле AJAX, которым необходимо получать данные с сервера. Загрузка новых данных может занять некоторое время. Без обратных вызовов весь пользовательский интерфейс "зависнет" при загрузке новых данных, или вам нужно будет обновить всю страницу, а не только ее часть. С помощью функции обратного вызова вы можете вставить «загружаемое» изображение и заменить его новыми данными после загрузки.

Некоторый код без обратного вызова:

function grabAndFreeze() {
    showNowLoading(true);
    var jsondata = getData('http://yourserver.com/data/messages.json');
    /* User Interface 'freezes' while getting data */
    processData(jsondata);
    showNowLoading(false);
    do_other_stuff(); // not called until data fully downloaded
}

function processData(jsondata) { // do something with the data
   var count = jsondata.results ? jsondata.results.length : 0;
   $('#counter_messages').text(['Fetched', count, 'new items'].join(' '));
   $('#results_messages').html(jsondata.results || '(no new messages)');
}

с обратным вызовом:

Вот пример с обратным вызовом, использующим jQuery getJSON :

function processDataCB(jsondata) { // callback: update UI with results
   showNowLoading(false);
   var count = jsondata.results ? jsondata.results.length : 0;
   $('#counter_messages').text(['Fetched', count, 'new items'].join(' '));
   $('#results_messages').html(jsondata.results || '(no new messages)');
}

function grabAndGo() { // and don't freeze
    showNowLoading(true);
    $('#results_messages').html(now_loading_image);
    $.getJSON("http://yourserver.com/data/messages.json", processDataCB);
    /* Call processDataCB when data is downloaded, no frozen User Interface! */
    do_other_stuff(); // called immediately
}

с закрытием:

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

/* Grab messages, chat users, etc by changing dtable. Run callback cb when done.*/
function grab(dtable, cb) { 
    if (null == dtable) { dtable = "messages"; }
    var uiElem = "_" + dtable;
    showNowLoading(true, dtable);
    $('#results' + uiElem).html(now_loading_image);
    $.getJSON("http://yourserver.com/user/"+dtable+".json", cb || function (jsondata) {
       // Using a closure: can "see" dtable argument and uiElem variables above.
       var count = jsondata.results ? jsondata.results.length : 0, 
           counterMsg = ['Fetched', count, 'new', dtable].join(' '),
           // no new chatters/messages/etc
           defaultResultsMsg = ['(no new ', dtable, ')'].join(''); 
       showNowLoading(false, dtable);
       $('#counter' + uiElem).text(counterMsg);
       $('#results'+ uiElem).html(jsondata.results || defaultResultsMsg);
    });
    /* User Interface calls cb when data is downloaded */

    do_other_stuff(); // called immediately
}

Использование:

// update results_chatters when chatters.json data is downloaded:
grab("chatters"); 
// update results_messages when messages.json data is downloaded
grab("messages"); 
// call myCallback(jsondata) when "history.json" data is loaded:
grab("history", myCallback); 

Закрытие

Наконец, вот определение closure из Дуглас Крокфорд :

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

Смотри также:

37 голосов
/ 08 октября 2015

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

Оба способа включают настройку функции путем передачидополнительная функциональность (определение функции, анонимная или именованная) для существующей функции.то есть.

customizableFunc(customFunctionality)

Если пользовательские функции просто подключены к блоку кода, вы настроили функцию, например, так.

    customizableFucn(customFunctionality) {
      var data = doSomthing();
      customFunctionality(data);
      ...
    }

Хотя этот вид встроенных функций часто называют "обратный вызов ", в этом нет ничего случайного.Очень очевидным примером является метод forEach, в котором пользовательская функция предоставляется в качестве аргумента для применения к каждому элементу в массиве для изменения массива.

Но это принципиально отличается от использования «обратного вызова»функции для асинхронного программирования , как в AJAX или node.js, или просто в назначении функциональности событиям взаимодействия с пользователем (например, щелчки мышью).В этом случае вся идея состоит в том, чтобы дождаться наступления условного события, прежде чем выполнять пользовательские функции.Это очевидно в случае взаимодействия с пользователем, но также важно в процессах ввода / вывода (ввода / вывода), которые могут занимать время, например, чтение файлов с диска.Именно здесь термин «обратный вызов» имеет наиболее очевидный смысл.После запуска процесса ввода-вывода (например, запрос на чтение файла с диска или на сервер для возврата данных из http-запроса) асинхронная программа не ожидает завершения его завершения.Он может выполнять любые задачи, запланированные далее, и отвечать пользовательскими функциями только после того, как он получит уведомление о том, что прочитанный файл или запрос http завершен (или что он не выполнен) и что данные доступны для пользовательской функции.Это все равно что позвонить в офис по телефону и оставить свой номер «обратного вызова», чтобы они могли звонить вам, когда кто-то готов ответить вам.Это лучше, чем просто повесить трубку, если кто знает, как долго и не может заниматься другими делами.

Асинхронное использование по своей природе подразумевает некоторые способы прослушивания нужного события (например, завершение ввода-вывода).процесс), чтобы, когда это происходит (и только когда это происходит), выполнялась пользовательская функция «обратного вызова».В очевидном примере AJAX, когда данные фактически поступают с сервера, запускается функция «обратного вызова», которая использует эти данные для изменения DOM и, следовательно, перерисовывает окно браузера до такой степени.

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

26 голосов
/ 12 марта 2012

С точки зрения непрограммиста, обратный вызов - это заполнение пробела в программе.

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

  • Каждый получает одну и ту же пустую форму, но
  • Каждый может написать другой номер экстренного контакта.

Это ключ. Вы не меняете форму (код, как правило, чужой). Однако вы можете заполнить недостающие фрагменты информации ( ваш номер).

Пример 1:

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

В этом примере вы не используете карандаш для заполнения бланка на листе бумаги, вы используете функцию set_print_callback(the_callback).

  • Пустой переменной в модуле / коде является пустая строка,
  • set_print_callback это карандаш,
  • и the_callback - это информация, которую вы вводите.

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

Пример 2:

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

Программирование в Gui работает практически так же. При нажатии кнопки программа должна выяснить, что делать дальше. Идет и ищет обратный звонок. Этот обратный вызов находится в пустом месте с надписью «Вот что вы делаете, когда нажата кнопка1»

Большинство IDE автоматически заполнят для вас бланк (напишите основной метод), когда вы попросите его (например, button1_clicked). Однако, этот бланк может иметь любой метод, который вы чертовски хорошо, пожалуйста, . Вы можете вызывать метод run_computations или butter_the_biscuits, если вы поместите имя этого обратного вызова в надлежащее поле. Вы можете поставить "555-555-1212" на номер службы экстренной помощи пустым. Это не имеет особого смысла, но это допустимо.


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

21 голосов
/ 11 марта 2012

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

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

20 голосов
/ 07 марта 2012

Всегда лучше начать с примера :).

Предположим, у вас есть два модуля A и B.

Вы хотите, чтобы модуль A был уведомлен , когда в модуле B. происходит какое-то событие / условие. Однако модуль B имеетпонятия не имею о вашем модуле A. Все, что он знает, - это адрес конкретной функции (модуля A) через указатель функции, который предоставляется ему модулем A.

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

*) Очевидным преимуществом здесь является то, что вы абстрагируете все о модуле A от модуля B. Модуль B не должен заботиться о том, кто / что является модулем A.

18 голосов
/ 11 марта 2012

Представьте, что вам нужна функция, которая возвращает 10 в квадрате, поэтому вы пишете функцию:

function tenSquared() {return 10*10;}

Позже вам нужно 9 в квадрате, чтобы написать другую функцию:

function nineSquared() {return 9*9;}

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

function square(x) {return x*x;}

Точно такое же мышление применимо к обратным вызовам. У вас есть функция, которая что-то делает, и когда она завершается, вызывается doA:

function computeA(){
    ...
    doA(result);
}

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

function computeB(){
    ...
    doB(result);
}

Или вы можете передать функцию обратного вызова в качестве переменной, и ее нужно иметь только один раз:

function compute(callback){
    ...
    callback(result);
}

Тогда вам просто нужно вызвать compute (doA) и compute (doB).

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

11 голосов
/ 11 марта 2012

Вы чувствуете себя плохо, поэтому вы идете к врачу.Он осматривает вас и определяет, что вам нужны лекарства.Он прописывает лекарства и вызывает рецепт в местной аптеке.Ты идешь домой.Позже ваша аптека звонит, чтобы сообщить вам, что ваш рецепт готов.Иди и забери это.

10 голосов
/ 11 марта 2012

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

Аналогия с ожиданием прибытия посылки, которая использовалась другими ответами, хороша для объяснения того и другого. В компьютерной программе вы бы сказали компьютеру ожидать посылку. Обычно он теперь сидел бы там и ждал (и больше ничего не делал), пока посылка не прибудет, возможно, на неопределенный срок, если она никогда не прибудет. Для людей это звучит глупо, но без дальнейших мер это совершенно естественно для компьютера.

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

...