Коллбэки. Асинхронные. Неблокирующие. Давайте говорить начистоту: все эти JS-концепции заставляют вас рвать волосы на голове каждый раз, когда ваш код СНОВА не работает. Меня тоже посещали подобные чувства. Мне нужна была какая-то простая аналогия, которая помогла бы мне легче понять эту абстрактную идею. Конечно, в сети есть много хороших учебных материалов (например, этот, или этот). Но все они обычно сразу начинаются с довольно сложных вещей.
Мне нужно было что-то более близкое, понятное.
Я собираюсь объяснить всем желающим, что такое коллбэки, на примере этих забавных существ. В этой моей аналогии читатели будут выступать в качестве повелителя миньонов. Вы можете приказывать им сделать в вашем коде всё, что угодно. НО!
- Существует только один повелитель.
- Миньоны должны получать приказы от вас. Они не могут принимать собственные решения.
(Официальное определение миньона: “некто слабый и неважный, исполняющий приказы сильного лидера или босса”)
Основная идея
Каждый раз, когда в jQuery или JavaScript вы видите “function()” внутри другой функции или метода, представляйте, что вместо неё написано “minion()”. Конечно, вы не сможете так написать, поскольку JS не распознает эту команду (если только вы не создадите настоящую функцию “minion”). Но при создании коллбэка вы, фактически, отдаёте приказ миньонам.
Пример миньонов, ждущих ваших приказов:
function myFunction(input, function(err, data){ });
Это можно перефразировать в:
function myFunction(input, minion(err, data){ });
Пример с обычной функцией, безо всяких миньонов:
function addOne(data){ return data++; };
Примеры в jQuery
Основы
Пример 1:
$('.myButton').click(function(){ $('.secondEl').show(); });
Напоминаю, что это можно перефразировать:
Пример 2:
$('.myButton').click(minion(){ $('.secondEl').show(); });
Что тут делает коллбэк?
Вы повелитель, и должны присматривать за событиями, происходящими в рамках всего файла, а то и нескольких файлов. У вас нет времени на возню с каким-то мелким обработчиком клика в jQuery! Поэтому вы возлагаете эту задачу на миньона, как показано в примере 2. Теперь это очень простая функция, возможно, вы бы даже сами смогли её сделать. А если бы она была длиной в 20 строк? Вам нельзя отвлекаться на 20-строчную функцию, ведь вам нужно принять от пользователя и другие инструкции! Поэтому вы говорите миньону, чтобы он занялся этим с первой же строки, как только пользователь кликнет .myButton
. Теперь вы можете отдавать приказы и другим миньонам, что куда эффективнее, чем всё делать самому, заставляя важные функции ожидать, пока вы освободитесь.
Анимация
Чтобы подчеркнуть важность миньонов, давайте рассмотрим последовательность отображения/скрытия.
$('button').click(function(){ console.log("One"); $('.firstChild').show(function(){ console.log("Two"); $('.childofChild').show(); }); console.log("Three"); });
Если читать код по порядку и не привлекать на свою сторону миньонов, то в консоли будет отображаться “One”, “Two”, “Three”. НО, если у вас есть миньоны, то в консоли отобразится “One”, “Three”, “Two”. И вот почему:
$('button').click(minion(){ console.log("One"); $('.firstChild').show(minion(){ console.log("Two"); $('.childofChild').show(); }); console.log("Three"); });
- Строка 1: вы отдали приказ первому миньону и отправились посмотреть на другие события, инициализированные пользователем.
- Строка 2: первый миньон считал состояние консоли и отправился на строку 3.
- Строка 3: первый миньон позвал на помощь второго миньона. Этот второй миньон должен сидеть тут и ждать, когда завершится метод
show()
, после чего можно будет продолжить выполнение инструкций. Так что теперь на вас работает два миньона, одновременно пытающихся как можно скорее завершить свою работу с функцией! - Первый миньон перепрыгивает на строку 7, а второму надо ещё выполнить строки 4 и 5. Он считывает состояние console.log, и готово — больше никакой работы не осталось. Второй миньон отстаёт на доли миллисекунды, он считывает console.log (“Two”), а потом убеждается, что в строке 5 отобразился дочерний
div
. Теперь и этот миньон завершил свою работу.
Отсюда можно вынести невероятно важный урок: ваши коллбэки определяют порядок осуществления различных действий. Только представьте, насколько это мощный инструмент. Вы может точно задавать очерёдность тех или иных событий, в отличие от необходимости создавать одну длинную строку из последовательных команд. Коллбэки дают гораздо больше гибкости. Если вы не смогли заставить миньонов выполнить ваши приказы, то придётся всё делать самостоятельно.
По сути, вышеупомянутая логика jQuery работает только применительно к коллбэкам. Например, в строке дочерний div
должен быть отображён после отображения родительского div
‘а. Вы не сможете отобразить дочерний, если родительский скрыт. И коллбэки — единственный способ гарантировать, что дочерний div
появится после родительского div
‘а.
Если бы в вышеприведённом примере не было коллбэков, то строка 5 стала бы источником ошибки, поскольку метод show()
в строке 3 ещё не успел выполниться. Первый миньон, запущенный в строке 1, передаёт задачу второму миньону по мере выполнения строк 4 и 5, так что второй миньон может ждать завершения метода show()
в строке 3 до того, как начать работу в строках 4 и 5. Это гарантирует, что второй миньон начнёт и завершит выполнение второго show()
после завершения первого show()
. К тому времени первый миньон перейдёт к оставшейся части внешней функции, не ожидая выполнения предыдущих операций.
Ванильные примеры из JavaScript/Node.js
Использование параметров и коллбэков
// обобщённый тип (generic) reportOrders отражает ваши приказы для функции function reportOrders (minionOrders) { if ( typeof minionOrders === "string"){ console.log(minionOrders); } else if ( typeof minionOrders === "object"){ for (var item in minionOrders) { console.log(item + ": " + minionOrders[item]); } } } // Функция получает два параметра, последний из которых — коллбэк function speakOrders (orders, callback) { callback (orders); } // Когда мы вызываем функцию speakOrders, мы передаём reportOrders в качестве параметра. // Так что reportOrders будет вызываемой обратно функцией (не исполняемой) внутри функции speakOrders speakOrders ({name:"Minion1031", speciality:"Scribe"}, reportOrders); // Console // name: Minion1031 // speciality: Scribe
Теперь более сложные примеры! Строки 2 и 14 — это всего лишь объявление функций, так что перейдём к строке 20, где начинается вся движуха. Вызовем функцию speakOrders
с двумя параметрами. Первый параметр — объект с состояниями, о которых вы хотите получать отчёты от своих миньонов. Второй параметр — коллбэк под названием reportOrders
.
Ваш миньон не может выполнить reportOrders
до тех пор, пока вы не дадите ему такой приказ. Именно так и выполняется эта функция. Вызовем speakOrders
с инструкциями в строке 20. Перейдём на строку 14 и посмотрим, что же делает функция speakOrders
. Очевидно, что она передаёт коллбэку свои инструкции.
В строке 20 функция reportOrders
объявлена коллбэком, но им может быть кто угодно. memorizeOrders
, tellMySpouse
, вы можете дать функции любое имя. Использование в объявлении функции в строке 14 слова “callback” считается хорошим тоном, чтобы другие люди могли взглянуть на код и понять, что к чему. Но можно использовать и любое другое слово! Вот миньонифицированный пример:
// обобщённый тип (generic) reportOrders отражает ваши приказы для функции function reportOrders (minionOrders) { if ( typeof minionOrders === "string"){ console.log(minionOrders); } else if ( typeof minionOrders === "object"){ for (var item in minionOrders) { console.log(item + ": " + minionOrders[item]); } } } // Функция получает два параметра, последний из которых — коллбэк function speakOrders (orders, minion>) { minion(orders); } // Когда мы вызываем функцию speakOrders, мы передаём reportOrders в качестве параметра. // Так что reportOrders будет вызываемой обратно функцией (не исполняемой) внутри функции speakOrders the speakOrders function speakOrders ({name:"Minion1031", speciality:"Scribe"}, reportOrders); // Console // name: Minion1031 // speciality: Scribe
В обеих строках 14-15 присутствует лишь один миньон, заменяющий “callback”.
Строка 20: Вызовем speakOrders
. Передадим именованный объект и назначение. Вторым параметром может быть что угодно — строковое, функция и т.п.
Строка 14–15: Определим, что в качестве второго параметра должен быть коллбэк, это minion(). При каждом вызове функции speakOrders
мы будем знать, что второй параметр будет функцией. В нашем случае — reportOrders
.
Строка 15: Из строки 20 мы знаем, что ваш миньон должен позаботиться о функции reportOrders
. Он получает параметры приказа — объект. Ему нужны эти инструкции для успешного создания отчёта.
Строка 2: Переменная приказа из строки 15 теперь ссылается внутри функции как minionOrders
. Выполнение функции reportOrders
завершается, а имя и назначение передаются обратно.
Здесь коллбэки выполняют важную роль чёткого отслеживания пути, по которому должен проследовать объект. Без них код превратился бы в монолитную строго упорядоченную кучу, без какой-либо гибкости с точки зрения повторного использования функций или изменения порядка выполнения.
Node.js
Взгляните на следующий пример, в котором используется Express и модуль запроса. Пока что это самое трудное!
var request = require('request'); var app = require('express')(); var results; function logRes(){ console.log(results); } app.get('/storeData', function(req,res){ readResult(logRes) }); function readResult(callback){ request('http://someroute.com/api', function(err, response, body){ results=body; callback(); }); }
Представим, что пользователь просто отправил запрос GET по маршруту /storeData
. Начнём со строки 9. В этом примере используются все ранее рассмотренные варианты использования коллбэков.
- В строке 9 есть в методе применяется коллбэк, аналогичный разобранному в примере про обработчика кликов в jQuery.
- В строке 14 применяется асинхронное исполнение, связанное с запросом фальшивому API. Это из примера про анимацию в jQuery.
- Наконец, в строке 13 объявляется коллбэк-параметр, аналогичный примеру с ванильным JS.
Чтобы не было недопонимания, вот миньонифицированный пример, в котором миньонам присвоены номера в соответствии с очерёдностью исполнения.
var request = require('request'); var app = require('express')(); var results; function logRes(){ console.log(results); } app.get('/storeData', minion1>(req,res){ readResult(logRes) }); function readResult(minion3){ request('http://someroute.com/api', minion2(err, response, body){ results=body; minion3(); }); }
- Строка 9: Пользователь отправляется по маршруту. Вы босс, вы даёте приказы первому миньону. Тот отправляется в строку 10 и видит функцию
readResult
. Теперь, пока миньоны работают, вы можете ожидать других сигналов от пользователя. - Строка 14: Первый миньон видит запрос, отправляет его фальшивому API и приказывает второму миньону ждать ответ. Теперь первый миньон может переходить к другой работе. А раз делать больше нечего, он уходит в отставку.
- Строка 14: Второй миньон начинает действовать по завершении запроса. Теперь у него есть три части потенциально важной информации:
err
,response
,body
. - Строка 15–16: Глобальной переменной “results” присваивается значение body. Теперь эта переменная может использоваться в других функциях. Второй миньон говорит третьему, что пришла пора обработать инструкции, которые третий миньон получил из строки 10. Всё это время он ожидал их выполнения, и теперь пришла пора выполнить
logRes()
! - Строка 5: А инструкции — это… console.log. Разочаровывает. В любом случае, третий миньон свою работу выполнил.
Так как же третий миньон был вызван после второго?
Если мы вернёмся в самое начало, к самому первому примеру, то увидим инициализацию коллбэка в строке 13. Это означает, что при каждому вызове функции readResult()
должен иметься коллбэк-параметр. Позднее этот коллбэк применяется в строке 16, где он использует результат запроса к API из строки 14, потому что у самого запроса есть коллбэк!
Представим, что коллбэк (третий миньон) находился бы ниже 17 строки, за пределами области видимости запроса. В этом случае он был бы вторым миньоном, поскольку выполнялся бы до завершения запроса. При этом ответ на запрос ещё не был бы получен, что лишает смысла всю функцию. Суть в том, чтобы сначала выполнить запрос, а потом передать полученный ответ.
Ещё раз: использование в функции readResult()
двух отдельных запросов позволяет удостовериться, что третий миньон начинает работать после выполнения запроса. Коллбэк предоставляет вам контроль, позволяющий отдать этот конкретный приказ.
Заключение
Вы повелитель миньонов, под вашим началом орды пищащих маленьких слуг, готовых исполнить ваши желания. Если вы можете дать им верные инструкции, то они смогут существенно облегчить вашу жизнь, выполняя за вас всё самое трудное.
ссылка на оригинал статьи https://habrahabr.ru/post/282015/
Добавить комментарий