# Ад обратных вызовов
*Руководство по созданию асинхронных программ на JavaScript.*
### Что такое «ад обратных вызовов»?
Асинхронный JavaScript или JavaScript, в котором используются обратные вызовы — это то, в чём трудно интуитивно разобраться. В основном код заканчивается вот так:
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err);
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename);
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err);
} else {
console.log(filename + ' : ' + values);
var aspect = (values.width / values.height);
widths.forEach(function (width, widthIndex) {
var height = Math.round(width / aspect);
console.log('resizing ' + filename + 'to ' + height + 'x' + height);
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function (err) {
if (err) {
console.log('Error writing file: ' + err);
}
});
}.bind(this));
}
});
});
}
});
Видите форму пирамиды и все эти `})` в конце? Фу! Это ласково называют **адом обратных вызовов** (callback hell).
Причина ада кроется в том, что люди пытаются писать JavaScript так, будто его выполнение происходит последовательно сверху вниз. Многие разработчики делают эту ошибку. В других языках, таких как C, Ruby или Python многие ожидают, что происходящее на строке №1 завершится перед тем, как выполнение перейдёт на строку №2 и так далее вниз до конца файла. Как вы дальше узнаете, JavaScript работает иначе.
## Что такое обратные вызовы?
Обратные вызовы (или колбэки) — общепринятое название для функций в JavaScript. Это не особая функция, которая называется «обратный вызов», а соглашение. Вместо того, чтобы сразу же вернуть какой-то результат, как делает большинство функций, эти использующие обратные вызовы функции требуют время для получения результата. Слово «асинхронный» или «async» значит, что оно «занимает какое-то время» или «случится в будущем, не сейчас». Обычно обратные вызовы используются только при выполнении ввода-вывода (I/O — ввод-вывод), например, при загрузке, чтении файлов, обмене информации с базами данных и так далее.
Когда вы вызываете обычную функцию, вы можете использовать её возвращаемое значение:
var result = multiplyTwoNumbers(5, 10);
console.log(result);
// выведется 50
Однако асинхронные функции, которые используют обратные вызовы, ничего сразу же не возвращают.
var photo = downloadPhoto('http://coolcats.com/cat.gif');
// photo равно 'undefined'!
В этом случае скачивание файла займёт очень много времени, и никто не хочет, чтобы программа была приостановлена (то есть заблокирована), пока идёт загрузка.
Вместо этого вы сохраняете в функцию код, который должен выполняться после завершения загрузки. **Это и есть обратный вызов!** Адрес изображения передаётся функции `downloadPhoto`, и она запускает наш обратный вызов `handlePhoto`, уже после скачивания.
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto);
function handlePhoto (error, photo) {
if (error) {
console.error('Ошибка загрузки!', error);
} else {
console.log('Загрузка завершена', photo);
}
}
console.log('Загрузка начата');
Самая большая проблема при попытке понять функции обратного вызова — это отсутствие знаний в каком порядке исполняются функции во время работы программы. В нашем примере произошло три крупных события. Сначала объявлена функция `handlePhoto`, потом запущена функция `downloadPhoto`, и ей передана `handlePhoto` в качестве функции обратного вызова. Наконец, было показано сообщение `'Загрузка начата'` на экран.
Обратите внимание, что `handlePhoto` не была вызвана сразу, она просто создана и передаётся в качестве обратного вызова в `downloadPhoto`. Но она не будет выполнена до тех пор, пока `downloadPhoto` не завершит выполнение своей задачи, которая может занять длительное время в зависимости от того, насколько быстрое у вас подключение к интернету.
Этот пример предназначен для демонстрации двух важных понятий:
- Обратный вызов `handlePhoto` — это просто способ отложить какие-то дела на более позднее время.
- Порядок, в котором всё происходит, не идёт сверху вниз — он перескакивает в зависимости от того, когда завершаются действия.
## Как выбраться из ада обратных вызовов?
Основная причина возникновения такой ситуации — недостаток практики, но, к счастью, писать хороший код несложно.
Вам нужно всего лишь следовать **трём правилам**:
## 1. Не используйте большую вложенность
Вот пример неряшливого кода, который использует [browser-request](https://github.com/iriscouch/browser-request), чтобы создать AJAX-запрос на сервер:
var form = document.querySelector('form');
form.onsubmit = function (submitEvent) {
var name = document.querySelector('input').value;
request({
uri: 'http://example.com/upload',
body: name,
method: 'POST'
}, function (err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) {
return statusMessage.value = err;
}
statusMessage.value = body;
});
};
Этот код имеет две анонимные функции. Давайте дадим им имена.
var form = document.querySelector('form');
form.onsubmit = function formSubmit (submitEvent) {
var name = document.querySelector('input').value;
request({
uri: 'http://example.com/upload',
body: name,
method: 'POST'
}, function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) {
return statusMessage.value = err;
}
statusMessage.value = body;
});
};
Как видите, давать имена функциям легко, и это имеет неоспоримые преимущества:
- Код легче читается, благодаря описательным именам функций.
- При исключениях вы получите стек (stack trace), который будет ссылаться на фактические имена функций вместо «анонимных».
- Позволяет перемещать функции и ссылаться на них по именам.
Теперь вы можете переместить функцию на верхний уровень программы:
document.querySelector('form').onsubmit = formSubmit;
function formSubmit (submitEvent) {
var name = document.querySelector('input').value;
request({
uri: 'http://example.com/upload',
body: name,
method: 'POST'
}, postResponse);
}
function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) {
return statusMessage.value = err;
}
statusMessage.value = body;
}
Обратите внимание, что объявление функций здесь было осуществлено в самом низу файла. Это произошло благодаря [поднятию функций](https://gist.github.com/maxogden/4bed247d9852de93c94c) (function hoisting).
## 2. Модульность
Это самая важная часть: *«Любой способен создавать модули (или библиотеки)»*. Цитируя [Исаака Шлютера](http://twitter.com/izs) (из проекта Node.js): *«Пишите маленькие модули, каждый из которых будет выполнять одну функцию и собирайте их в более крупные модули. Вы не сможете попасть в ад обратных вызовов, если не пойдёте туда сами»*.
Давайте возьмём наш шаблонный код сверху и превратим в модуль, разделив на несколько файлов. Я покажу как сделать модульный паттерн, который работает с любым браузером или сервером:
Назовём наш новый файл `formuploader.js`, он содержит в себе две функции:
module.exports.submit = formSubmit;
function formSubmit (submitEvent) {
var name = document.querySelector('input').value;
request({
uri: 'http://example.com/upload',
body: name,
method: 'POST'
}, postResponse);
}
function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) {
return statusMessage.value = err;
}
statusMessage.value = body;
}
`module.exports` — это небольшой пример модуля, который работает в Node.js, Electron и в браузере через [browserify](https://github.com/substack/node-browserify). Мне очень нравится этот стиль для модулей, так как он работает везде, очень лёгок в понимании и не требует сложных конфигурационных файлов и скриптов.
Теперь, когда у нас есть `formuploader.js` (он будет загружен на страницу в тег script после работы browserify), нам просто нужно вызвать и использовать его:
var formUploader = require('formuploader');
document.querySelector('form').onsubmit = formUploader.submit;
В результате наше приложение имеет только две строки кода и обладает следующими преимуществами:
- Лёгкость понимания для новых разработчиков — у них не будет трудностей с чтением файла `formuploader`.
- `formuploader` может быть переиспользован без дублирования кода и легко выложен на Гитхаб или в npm.
## 3. Обрабатывайте каждую ошибку
Существуют различные типы ошибок: синтаксические ошибки (обычно появляются при первом запуске программы), ошибки во время исполнения (код начал выполняться, но какая-то ошибка всё испортила), неверное расширение файла, сбой в работе жёсткого диска, отсутствие интернет-соединения и так далее.
Соблюдение первых двух правил делает ваш код не только легко читаемым, но и устойчивым к ошибкам. Когда мы имеем дело с обратными вызовами, мы по определению имеем дело с задачами, которые отправились на выполнение, запустились, сделали что-то в фоновом режиме и успешно завершились или прервались из-за ошибки. Любой опытный разработчик подтвердит, что вы никогда не сможете предугадать, когда эти ошибки произойдут, поэтому всегда нужно обрабатывать их, для предотвращения таких случаев.
У обратных вызовов самым популярным соглашением обработки ошибок является стиль Node.js, где первый параметр всегда резервируется для ошибок.
var fs = require('fs');
fs.readFile('/Не/сущес/твует', handleFile);
function handleFile (error, file) {
if (error) {
return console.error('Ой, случилась ошибка', error);
}
// в противном случае, продолжай и используй `file` в своём коде
}
Первый аргумент `error` не даст вам забыть обработать свои ошибки. Если бы это был второй аргумент, то вы могли написать код так: `function handleFile (file) { }` и легко пропустить ошибку.
Также можно настроить линтеры кода, они помогут не забывать обрабатывать ошибки при обратных вызовах. Самый простой для использования — [standard](http://standardjs.com/). Всё что нужно сделать — это запустить команду `$ standard` в своей папке с кодом, и он покажет каждый обратный вызов с необработанной ошибкой.
### Краткий итог:
1. Избегайте большой вложенности функций. Дайте им имена и разместите их на верхнем уровне программы.
2. Используйте [поднятие функций](https://gist.github.com/maxogden/4bed247d9852de93c94c) чтобы переместить объявление функций в нижнюю часть страницы.
3. Обрабатывайте **каждую ошибку** в каждом обратном вызове. Для этого подойдёт линтер [standard](http://standardjs.com/).
4. Создавайте переиспользуемые функции и помещайте их в модули, чтобы сократить время для понимания вашего кода. Разделяйте код на маленькие кусочки — это поможет обрабатывать ошибки, писать тесты, заставит вас создавать стабильный, задокументированный публичный API и облегчит его рефакторинг.
Наиболее важный аспект для избавления от ада обратных вызовов — перемещение функций за пределы основной последовательности действий программы так, чтобы её поток выполнения был более понятен, и новичкам не было необходимости разбираться со всеми деталями функций, чтобы уловить суть того, что программа пытается сделать.
Можете начать перемещать функции в конец файла, затем переместить их в другой файл, который вы будете загружать как `require('./photo-helpers.js')`, а потом переместить их в отдельный модуль `require('image-resize')`.
Вот некоторые эмпирические правила при создании модуля:
- Начните с перемещения неоднократно использованного кода в функцию.
- Когда ваша функция (или группа функций связанных одной темой) получится достаточно большой, переместите её в другой файл и вызывайте используя `module.exports`. Вы можете загружать новый модуль, используя относительный вызов.
- Если у вас есть код, который может быть использован в нескольких проектах, создайте для него описание, тесты, `package.json` и опубликуйте на Гитхабе и в npm. У этого подхода есть много преимуществ, но я не буду перечислять их здесь.
- Хороший модуль — небольшой, и фокусируется на одной проблеме.
- В файле модуля не должно быть больше 150 строчек кода.
- Модуль не должен иметь более одного уровня вложенности папок или JavaScript-файлов. Если это произошло, то, вероятно, он делает слишком много всего.
- Попросите более опытных программистов показать примеры хороших модулей, чтобы у вас сложилось правильное представление о том, как они должны выглядеть. Если понимание того, что происходит в модуле занимает по времени больше чем несколько минут, то это не очень хороший модуль.
### Что читать?
Попробуйте прочесть моё [большое введение в обратные вызовы](https://github.com/maxogden/art-of-node#callbacks), или пройти несколько уроков в [NodeSchool](http://nodeschool.io/ru/).
Ещё почитайте [карманный справочник browserify](https://github.com/substack/browserify-handbook) в качестве примера того, как стоит писать модульный код.
### А что насчёт промисов, генераторов, ES6?
Помните, что обратные вызовы — фундаментальная часть JavaScript (поскольку являются просто функциями), и вы должны научиться читать и писать их прежде, чем переходить к более продвинутым функциям языка, так как все они зависят от понимания обратных вызовов. Если вы пока не можете написать удобный для поддержки код обратных вызовов, то продолжайте работать над этим.
Если вы действительно хотите, чтобы ваш асинхронный код читался сверху вниз, то есть некоторые необычные свойства, которые можно попробовать. Обратите внимание — **они могут снизить производительность и спровоцировать кроссплатформенные проблемы совместимости во время выполнения**, поэтому убедитесь, что всё предусмотрели.
**Промисы** — это путь написания асинхронного кода, который выглядит так, будто он исполняется сверху вниз и обрабатывает больше типов ошибок из-за использования `try/catch`.
**Генераторы** могут «приостановить» отдельную функцию без паузы для всей программы, за счёт чуть более сложного кода, они позволяют вашему асинхронному коду выполняться сверху вниз. Посмотрите на пример такого подхода — [watt](https://github.com/mappum/watt).
**Асинхронные функции** — предложенная особенность ES7, которая обернёт генераторы и промисы в высокоуровневый синтаксис. Если они вас заинтересовали, то можете изучить их подробнее.
Я использую обратные вызовы в 90% случаях при написании асинхронного кода и лишь когда всё становится сложно, перехожу на [run-parallel](https://github.com/feross/run-parallel) или [run-series](https://github.com/feross/run-series). Не думаю, что мне нужны обратные вызовы, промисы или что-то подобное, так как гораздо важнее сохранить код простым, не вложенным и разделённым на маленькие модули.
Независимо от метода, который вы выберите, всегда **обрабатывайте ошибки** и **делайте свой код простым**.
### Помните, только *вы* можете предотвратить ад обратных вызовов и лесные пожары.
Это перевод статьи Макса Одена — «[Callback Hell](http://callbackhell.com/)».
Вы можете найти исходники [перевода](https://github.com/htmlacademy/callback-hell) и [оригинала](https://github.com/maxogden/callback-hell) на Гитхабе.