20. Введение в асинхронную разработку

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

используя модуль fs, при получении запроса на такой ‘/’ url, считывается файл ‘index.html’ и выводится посетителю. Обращаю ваше внимание, ‘fs’ здесь взято для примера, вместо вот такого запроса — «fs.readFileSync(‘index.html’);», здесь мог быть запрос к базе данных или какая то другая операция, которая потребует существенного времени ожидания, в данном случае, это ожидание ответа от диска. Если бы это был запрос к базе данных, это было бы ожидание ответа по сети от базы. Наш код с одной стороны будет работать, с другой стороны в нем есть проблема, связана с масштабируемостью, которая неизбежно проявится в  серьезной, промышленной эксплуатации. Например, Петя зашел по этому url — ‘/’ и запросил файл —  «fs.readFileSync(‘index.html’);», Петя ждет пока сервер ему ответит и сервер ждет пока файл прочитается и готов ему выслать данные. В это время заходит Вася, Маша и куча другого народу, которые тоже хотят, что то от сервера, например они хотят не вот этот файл, а они хотят вообще, что то другое, скажем получить текущую дату, которую по идеи можно взять и тут же вернуть

Но сервер не может это сделать, поскольку сейчас, его интерпретатор javascript занят, он ожидает ответа от диска — «fs.readFileSync(‘index.html’);». Когда этот ответ получен он  может продолжить выполнение, и выполнить следующую строчку — «res.end(info);», закончить наконец обработку запроса и тогда JavaScript освободится и сможет обработать какие то еще запросы. В результате мы имеем ситуацию, когда одна операция требующая долгого ожидания фактически парализует работу сервера, что конечно же неприемлемо. Только поймите меня правильно, сам по себе вызов, вполне нормальный и такие — «fs.readFileSync(‘index.html’);» — синхронные вызовы замечательно работают если нам нужно делать консольный скрипт. В котором, например, мы должны  прочитать файл, потом там с ним что то сделать, потом там его куда то записать и так далее. То есть когда мы должны последовательно сделать ряд задач, связанных с файлами, то такие вызовы, это замечательно, это просто и удобно. Проблемы с ними возникают лишь в серверном окружении, когда нужно делать много вещей одновременно. По этому здесь нужно воспользоваться другим методом, который тоже есть в модуле «fs» и который работает Асинхронно. Иначе говоря, асинхронный метод сразу ничего не возвращает обычно, но вместо этого он инициирует чтение файла, получает аргумент-функцию которой он этот файл передаст когда закончит процесс —

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

задачу Node.JS и после этого выполнение продолжается. При этом просчитывать мы будем конечно же вот здесь

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

Функцию которую Node.JS обязуется вызвать когда завершит процесс, называют функцией обратного вызова или по английски — callback function, или просто callback. Важный подводный камень состоит в том, что о возможности ошибки при таком вызове, можно легко забыть. Например, посмотрим что будет, если  файл ‘index.html’, почему то отсутствует. Либо была какая то ошибка при чтении скажем, или с правами, или с диском. В этой ситуации модуль «fs» вызовет callback с первым аргументом, с объектом ошибки, а второго аргумента вообще не будет. Если мы ошибку никак не обрабатываем, то получится что посетитель получит вообще пустую строку. Вот как будто вот такой вызов

что в принципе работает так же как и вот эта

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

Но в данном случае будет более правильным сделать вот такой вариант

 

Итак, в завершении этой главы, сравним синхронный и асинхронный код используя для примера реализацию сервера.

Синхронный вариант

Асинхронный вариант

 

Начнем с синхронного. Синхронные вызовы, типа

Используются достаточно редко, они применяются в тех случаях, когда мы можем себе позволить заблокировать интерпретатор JavaScript. Как правило это значит, что нет параллелизма. Например консольный скрипт, сделай первое, второе, третье и так далее. Синхронный вызов заставляет наш интерпретатор ждать, потом он ответ пишет в «info», если какая то ошибка вышла, то это исключение и оно отлавливается при помощи try..catch.

Асинхронный вариант работает по другому. Тут видите другой вызов

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

в try..catch, не имеет особого смысла. То есть, конечно можно, но с другой стороны, при вызове «fs.readFile()», собственно ошибки никакой не возникнет.  Этот метод устроен так, что работает, что работает асинхронно и все ошибки передает в callback.  На эту тему в Node.JS есть соглашение, все встроенные модули ему следуют и мы тоже, что первый аргумент функции-обработчика является всегда ошибкой.

То есть вот эта функция, назову ее «cb» для наглядности

Будет при ошибке вызвана так

А если ошибки нет, то она будет вызвана так — «cb(null, ….)» первый аргумент будет null, а во втором уже будут какие то результаты.

Соответственно важное отличие между синхронным и асинхронным вариантом, здесь в том, что если мы в синхронном варианте, вдруг забыли try…catch, то при ошибке, это обязательно станет нам известным, исключение просто выпадет от сюда

и повалит процесс в данном коде.
А здесь

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

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