Что такое асинхронный код. Основы построения асинхронных приложений. Формирование и установка кода

Объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.

Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии - «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

О синхронном и асинхронном коде Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

Console.log("1") console.log("2") console.log("3")
Он, без особых сложностей, выводит в консоль числа от 1 до 3.

Теперь - код асинхронный:

Console.log("1") setTimeout(function afterTwoSeconds() { console.log("2") }, 2000) console.log("3")
Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout . Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds .

Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

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

В плане интерфейса ограничимся чем-нибудь простым .


Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch . И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

// аргумент url может быть чем-то вроде "https://api.github.com/users/daspinola/repos" function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { // Код обработки успешного завершения запроса } else { // Обрабатываем ответ с сообщением об ошибке } } } xhr.ontimeout = function () { // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию } xhr.open("get", url, true) xhr.send(); }
Обратите внимание на то, что в этих примерах важно не то, что в итоге придёт с сервера, и как это будет обработано, а сама организация кода при использовании разных подходов, которые вы сможете использовать в своих асинхронных разработках.

Функции обратного вызова С функциями в JS можно делать очень много всего, в том числе - передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:

// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis". doThis(andThenThis) // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию function andThenThis() { console.log("and then this") } // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант function doThis(callback) { console.log("this first") // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, "()", иначе ничего не получится callback() }
Используя этот подход для решения нашей задачи, мы можем написать такую функцию request:

Function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log("Timeout") } xhr.open("get", url, true) xhr.send(); }
Теперь функция для выполнения запроса принимает параметр callback , поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, function handleReposList(err, repos) { if (err) throw err //Здесь обработаем список репозиториев }) }) })
Разберём то, что здесь происходит:

  • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
  • После завершения запроса вызывается коллбэк handleUsersList ;
  • Если не было ошибок, разбираем ответ сервера c помощью J SON.parse , преобразовываем его, для удобства, в объект;
  • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url - это URL для наших следующих запросов, и получили мы его из первого запроса.
  • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList . Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.
Обратите внимание на то, что использование в качестве первого параметра объекта ошибки - это широко распространённая практика, в частности, для разработки с использованием Node.js.

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

Try { request(userGet, handleUsersList) } catch (e) { console.error("Request boom! ", e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err // Здесь обрабатываем список репозиториев console.log("My very few repos", repos) }
Этот подход работает, но используя его, мы рискуем столкнуться с проблемами вроде состояния гонки запросов и сложностей с обработкой ошибок. Однако, основная неприятность, связанная с коллбэками, которых, считая то, что происходит в цикле forEach , здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.


Ад коллбэков во всей красе. Изображение взято отсюда .

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

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

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

Const myPromise = new Promise(function(resolve, reject) { // Здесь будет код if (codeIsFine) { resolve("fine") } else { reject("error") } }) myPromise .then(function whenOk(response) { console.log(response) return response }) .catch(function notOk(err) { console.error(err) })
Разберём этот пример:

  • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject ;
  • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise . Если код будет выполнен успешно, вызывают метод resolve , если нет - reject ;
  • Если функция вызовет resolve , будет исполнен метод.then для объекта Promise , аналогично, если будет вызван reject , будет исполнен метод.catch .
Вот что стоит помнить, работая с промисами:
  • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve("yey", "works") , коллбэку.then будет передано лишь "yey" ;
  • Если объединить в цепочку несколько вызовов.then , в конце соответствующих коллбэков следует всегда использовать return , иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
  • При выполнении команды reject , если следующим в цепочке идёт.then , он будет выполнен (вы можете считать.then выражением, которое выполняется в любом случае);
  • Если в цепочке из вызовов.then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение.catch ;
  • У промисов есть три состояния: «pending» - состояние ожидания вызова resolve или reject , а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve , и неуспешному, с вызовом reject , завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.
Обратите внимание на то, что промисы можно создавать без использования отдельно определённых функций, описывая функции в момент создания промисов. То, что показано в нашем примере - лишь распространённый способ инициализации промисов.

Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.

Function request(url) { return new Promise(function (resolve, reject) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject("timeout") } xhr.open("get", url, true) xhr.send(); }) }
При таком подходе, когда вы вызываете request , возвращено будет примерно следующее.

Это - промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

Теперь, воспользовавшись новой функцией request , перепишем остальной код.

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log("will be pending when logged", myPromise) myPromise .then(function handleUsersList(users) { console.log("when resolve is found it comes here with the response, in this case users ", users) const list = JSON.parse(users).items return Promise.all(list.map(function(user) { return request(user.repos_url) })) }) .then(function handleReposList(repos) { console.log("All users repos in an array", repos) }) .catch(function handleErrors(error) { console.log("when a reject is executed it will come here ignoring the then statement ", error) })
Здесь мы оказываемся в первом выражении.then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение.then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении.catch .

Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) { return request(user.repos_url) })) } function handleReposList(repos) { console.log("All users repos in an array", repos) } function handleErrors(error) { console.error("Something went wrong ", error) }
При таком подходе один взгляд на имена коллбэков в выражениях.then раскрывает смысл вызова userRequest . С кодом легко работать, его легко читать.

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

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

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

Function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) // выведет 1 console.log(fooIterator.next().value) // выведет 2 fooIterator.next("aParam") // приведёт к вызову console.log внутри генератора и к выводу "aParam"
Дело тут в том, что генераторы, вместо return , используют выражение yield , которое останавливает выполнение функции до следующего вызова.next итератора. Это похоже на выражение.then в промисах, которое выполняется при разрешении промиса.

Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

Function request(url) { return function(callback) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log("timeout") } xhr.open("get", url, true) xhr.send() } }
Тут, как обычно, мы используем аргумент url , но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.

Генератор будет выглядеть так:

Function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i console.log("after 2 seconds", result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => { setTimeout(() => { resolve(value) }, 2000); }); }
Здесь происходит следующее:

  • Имеется асинхронная функция sumTwentyAfterTwoSeconds ;
  • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds , который может завершиться вызовом resolve или reject ;
  • Выполнение кода заканчивается в.then , где завершается операция, отмеченная ключевым словом await , в данном случае - это всего одна операция.
Подготовим функцию request к использовании в конструкции async/await:

Function request(url) { return new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject("timeout") } xhr.open("get", url, true) xhr.send() }) }
Теперь создаём функцию с ключевым словом async , в которой используем ключевое слово await:

Async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) { const repos = await request(user.repos_url) handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) // Обрабатываем тут репозитории для каждого пользователя console.log(user, userRepos) }
Итак, у нас имеется асинхронная функция list , которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach , чтобы сформировать список репозиториев. Вызвать всё это очень просто:

List() .catch(e => console.error(e))
Этот подход и использование промисов - мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать .

Минус async/await , как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например - babel .

Итоги можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await . Если вы хотите как следует разобраться с тем, о чём мы говорили - поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch . Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик - буду благодарен, если расскажете об этом мне .

В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход - то, что вам нужно.

Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?

Теги:

Добавить метки

Объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.

Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии - «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

О синхронном и асинхронном коде Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

Console.log("1") console.log("2") console.log("3")
Он, без особых сложностей, выводит в консоль числа от 1 до 3.

Теперь - код асинхронный:

Console.log("1") setTimeout(function afterTwoSeconds() { console.log("2") }, 2000) console.log("3")
Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout . Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds .

Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

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

В плане интерфейса ограничимся чем-нибудь простым .


Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch . И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

// аргумент url может быть чем-то вроде "https://api.github.com/users/daspinola/repos" function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { // Код обработки успешного завершения запроса } else { // Обрабатываем ответ с сообщением об ошибке } } } xhr.ontimeout = function () { // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию } xhr.open("get", url, true) xhr.send(); }
Обратите внимание на то, что в этих примерах важно не то, что в итоге придёт с сервера, и как это будет обработано, а сама организация кода при использовании разных подходов, которые вы сможете использовать в своих асинхронных разработках.

Функции обратного вызова С функциями в JS можно делать очень много всего, в том числе - передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:

// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis". doThis(andThenThis) // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию function andThenThis() { console.log("and then this") } // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант function doThis(callback) { console.log("this first") // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, "()", иначе ничего не получится callback() }
Используя этот подход для решения нашей задачи, мы можем написать такую функцию request:

Function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log("Timeout") } xhr.open("get", url, true) xhr.send(); }
Теперь функция для выполнения запроса принимает параметр callback , поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, function handleReposList(err, repos) { if (err) throw err //Здесь обработаем список репозиториев }) }) })
Разберём то, что здесь происходит:

  • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
  • После завершения запроса вызывается коллбэк handleUsersList ;
  • Если не было ошибок, разбираем ответ сервера c помощью J SON.parse , преобразовываем его, для удобства, в объект;
  • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url - это URL для наших следующих запросов, и получили мы его из первого запроса.
  • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList . Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.
Обратите внимание на то, что использование в качестве первого параметра объекта ошибки - это широко распространённая практика, в частности, для разработки с использованием Node.js.

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

Try { request(userGet, handleUsersList) } catch (e) { console.error("Request boom! ", e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err // Здесь обрабатываем список репозиториев console.log("My very few repos", repos) }
Этот подход работает, но используя его, мы рискуем столкнуться с проблемами вроде состояния гонки запросов и сложностей с обработкой ошибок. Однако, основная неприятность, связанная с коллбэками, которых, считая то, что происходит в цикле forEach , здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.


Ад коллбэков во всей красе. Изображение взято отсюда .

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

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

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

Const myPromise = new Promise(function(resolve, reject) { // Здесь будет код if (codeIsFine) { resolve("fine") } else { reject("error") } }) myPromise .then(function whenOk(response) { console.log(response) return response }) .catch(function notOk(err) { console.error(err) })
Разберём этот пример:

  • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject ;
  • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise . Если код будет выполнен успешно, вызывают метод resolve , если нет - reject ;
  • Если функция вызовет resolve , будет исполнен метод.then для объекта Promise , аналогично, если будет вызван reject , будет исполнен метод.catch .
Вот что стоит помнить, работая с промисами:
  • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve("yey", "works") , коллбэку.then будет передано лишь "yey" ;
  • Если объединить в цепочку несколько вызовов.then , в конце соответствующих коллбэков следует всегда использовать return , иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
  • При выполнении команды reject , если следующим в цепочке идёт.then , он будет выполнен (вы можете считать.then выражением, которое выполняется в любом случае);
  • Если в цепочке из вызовов.then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение.catch ;
  • У промисов есть три состояния: «pending» - состояние ожидания вызова resolve или reject , а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve , и неуспешному, с вызовом reject , завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.
Обратите внимание на то, что промисы можно создавать без использования отдельно определённых функций, описывая функции в момент создания промисов. То, что показано в нашем примере - лишь распространённый способ инициализации промисов.

Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.

Function request(url) { return new Promise(function (resolve, reject) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject("timeout") } xhr.open("get", url, true) xhr.send(); }) }
При таком подходе, когда вы вызываете request , возвращено будет примерно следующее.

Это - промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

Теперь, воспользовавшись новой функцией request , перепишем остальной код.

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log("will be pending when logged", myPromise) myPromise .then(function handleUsersList(users) { console.log("when resolve is found it comes here with the response, in this case users ", users) const list = JSON.parse(users).items return Promise.all(list.map(function(user) { return request(user.repos_url) })) }) .then(function handleReposList(repos) { console.log("All users repos in an array", repos) }) .catch(function handleErrors(error) { console.log("when a reject is executed it will come here ignoring the then statement ", error) })
Здесь мы оказываемся в первом выражении.then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение.then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении.catch .

Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) { return request(user.repos_url) })) } function handleReposList(repos) { console.log("All users repos in an array", repos) } function handleErrors(error) { console.error("Something went wrong ", error) }
При таком подходе один взгляд на имена коллбэков в выражениях.then раскрывает смысл вызова userRequest . С кодом легко работать, его легко читать.

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

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

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

Function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) // выведет 1 console.log(fooIterator.next().value) // выведет 2 fooIterator.next("aParam") // приведёт к вызову console.log внутри генератора и к выводу "aParam"
Дело тут в том, что генераторы, вместо return , используют выражение yield , которое останавливает выполнение функции до следующего вызова.next итератора. Это похоже на выражение.then в промисах, которое выполняется при разрешении промиса.

Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

Function request(url) { return function(callback) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log("timeout") } xhr.open("get", url, true) xhr.send() } }
Тут, как обычно, мы используем аргумент url , но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.

Генератор будет выглядеть так:

Function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i console.log("after 2 seconds", result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => { setTimeout(() => { resolve(value) }, 2000); }); }
Здесь происходит следующее:

  • Имеется асинхронная функция sumTwentyAfterTwoSeconds ;
  • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds , который может завершиться вызовом resolve или reject ;
  • Выполнение кода заканчивается в.then , где завершается операция, отмеченная ключевым словом await , в данном случае - это всего одна операция.
Подготовим функцию request к использовании в конструкции async/await:

Function request(url) { return new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject("timeout") } xhr.open("get", url, true) xhr.send() }) }
Теперь создаём функцию с ключевым словом async , в которой используем ключевое слово await:

Async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) { const repos = await request(user.repos_url) handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) // Обрабатываем тут репозитории для каждого пользователя console.log(user, userRepos) }
Итак, у нас имеется асинхронная функция list , которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach , чтобы сформировать список репозиториев. Вызвать всё это очень просто:

List() .catch(e => console.error(e))
Этот подход и использование промисов - мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать .

Минус async/await , как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например - babel .

Итоги можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await . Если вы хотите как следует разобраться с тем, о чём мы говорили - поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch . Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик - буду благодарен, если расскажете об этом мне .

В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход - то, что вам нужно.

Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?

Теги:

  • JavaScript
  • разработка
  • callback
  • async
  • await
  • promise
  • generator
  • асинхронный код
Добавить метки

Мы особенно рекомендуем использовать асинхронный код загрузки, если ваши сайты построены на технологии AJAX. Так вы сможете настроить обновление содержимого рекламного блока при изменении контента страницы или другом указанном вами событии.

","contentType":"text/html"},"proposedBody":{"source":"

Мы особенно рекомендуем использовать асинхронный код загрузки, если ваши сайты построены на технологии AJAX. Так вы сможете настроить обновление содержимого рекламного блока при изменении контента страницы или другом указанном вами событии.

Мы особенно рекомендуем использовать асинхронный код загрузки, если ваши сайты построены на технологии AJAX. Так вы сможете настроить обновление содержимого рекламного блока при изменении контента страницы или другом указанном вами событии.

","contentType":"text/html"},"authorId":"119287251","slug":"457","canEdit":false,"canComment":false,"isBanned":false,"canPublish":false,"viewType":"old","isDraft":false,"isOnModeration":false,"isSubscriber":false,"commentsCount":21,"modificationDate":"Thu Jan 01 1970 03:00:00 GMT+0000 (UTC)","showPreview":true,"approvedPreview":{"source":"

Мы особенно рекомендуем использовать асинхронный код загрузки, если ваши сайты построены на технологии AJAX. Так вы сможете настроить обновление содержимого рекламного блока при изменении контента страницы или другом указанном вами событии.

","html":"Партнеры РСЯ могут ускорить загрузку рекламных блоков на своих сайтах с помощью нового асинхронного кода.","contentType":"text/html"},"proposedPreview":{"source":"

Мы особенно рекомендуем использовать асинхронный код загрузки, если ваши сайты построены на технологии AJAX. Так вы сможете настроить обновление содержимого рекламного блока при изменении контента страницы или другом указанном вами событии.

","html":"Партнеры РСЯ могут ускорить загрузку рекламных блоков на своих сайтах с помощью нового асинхронного кода.","contentType":"text/html"},"titleImage":null,"tags":[{"displayName":"асинхронный код","slug":"asinkhronnyy-kod","categoryId":"47982168","url":"/blog/partner??tag=asinkhronnyy-kod"},{"displayName":"партнерский интерфейс","slug":"partnerskiy-interfeys","categoryId":"25247736","url":"/blog/partner??tag=partnerskiy-interfeys"},{"displayName":"новость","slug":"novost","categoryId":"25247737","url":"/blog/partner??tag=novost"},{"displayName":"рекламные блоки","slug":"reklamnye-bloki","categoryId":"24650991","url":"/blog/partner??tag=reklamnye-bloki"}],"isModerator":false,"commentsEnabled":true,"url":"/blog/partner/457","urlTemplate":"/blog/partner/%slug%","fullBlogUrl":"https://yandex.ru/blog/partner","addCommentUrl":"/blog/createComment/partner/457","updateCommentUrl":"/blog/updateComment/partner/457","addCommentWithCaptcha":"/blog/createWithCaptcha/partner/457","changeCaptchaUrl":"/blog/api/captcha/new","putImageUrl":"/blog/image/put","urlBlog":"/blog/partner","urlEditPost":"/blog/56a93a7dd97351872e7341e0/edit","urlSlug":"/blog/post/generateSlug","urlPublishPost":"/blog/56a93a7dd97351872e7341e0/publish","urlUnpublishPost":"/blog/56a93a7dd97351872e7341e0/unpublish","urlRemovePost":"/blog/56a93a7dd97351872e7341e0/removePost","urlDraft":"/blog/partner/457/draft","urlDraftTemplate":"/blog/partner/%slug%/draft","urlRemoveDraft":"/blog/56a93a7dd97351872e7341e0/removeDraft","urlTagSuggest":"/blog/api/suggest/partner","urlAfterDelete":"/blog/partner","isAuthor":false,"subscribeUrl":"/blog/api/subscribe/56a93a7dd97351872e7341e0","unsubscribeUrl":"/blog/api/unsubscribe/56a93a7dd97351872e7341e0","urlEditPostPage":"/blog/partner/56a93a7dd97351872e7341e0/edit","urlForTranslate":"/blog/post/translate","urlRelateIssue":"/blog/post/updateIssue","urlUpdateTranslate":"/blog/post/updateTranslate","urlLoadTranslate":"/blog/post/loadTranslate","urlTranslationStatus":"/blog/partner/457/translationInfo","urlRelatedArticles":"/blog/api/relatedArticles/partner/457","author":{"id":"119287251","uid":{"value":"119287251","lite":false,"hosted":false},"aliases":{},"login":"i.alex.under","display_name":{"name":"i.alex.under","avatar":{"default":"20706/119287251-18411626","empty":false}},"address":"i.alex..mds.yandex.net/get-yapic/20706/119287251-18411626/islands-middle","isYandexStaff":false},"originalModificationDate":"1970-01-01T00:00:00.000Z","socialImage":{"orig":{"fullPath":"http://avatars.yandex.net/get-yablog/4611686018427441880/normal"}}}}}">

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

Базовое описание асинхронного кода

Основными функциями асинхронного кода JavaScript являются setTimeout и setInterval . Функция setTimeout выполняет заданную функцию после истечения определенного временного интервала. Она принимает возвратную функцию в качестве первого аргумента и время (в миллисекундах) в качестве второго аргумента. Вот пример использования:

Console.log("a"); setTimeout(function() { console.log("c") }, 500); setTimeout(function() { console.log("d") }, 500); setTimeout(function() { console.log("e") }, 500); console.log("b");

Ожидается, что в консоли мы увидим “a”, “b”, а затем через примерно 500 мс - “c”, “d”, и “e”. Я использую термин “примерно” потому, что в действительности setTimeout работает непредсказуемо. Даже в спецификации HTML5 указано: "Таким образом, API не гарантирует, что таймер выполнится точно по заданному расписанию. Вероятны задержки из-за нагрузки процессора, других задач и прочих факторов."

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

Цикл событий очередь возвратных функций. Когда асинхронная функция выполняется, возвратная функция ставится в очередь. JavaScript не запускает обработку цикла событий, пока код, запущенный после асинхронной функции выполняется. Данный факт означает, что код JavaScript не является многопоточным, хотя и кажется таковым. Цикл событий является очередью FIFO (первый пришел, первый вышел), что означает выполнение возвратных функций в порядке их поступления. JavaScript был выбран для платформы node.js именно по причине простого процесса разработки подобного кода.

AJAX

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

Асинхронное получение данных с помощью метода jQuery $.ajax является простым кросс-браузерным процессом, который скрывает реальный процесс. Например:

Обычно, но неправильно, предполагается, что данные станут доступны сразу после вызова $.ajax . Но действительность выглядит иначе:

Xmlhttp.open("GET", "some/ur/1", true); xmlhttp.onreadystatechange = function(data) { if (xmlhttp.readyState === 4) { console.log(data); } }; xmlhttp.send(null);

Схожий метод публикации событий используется в шаблоне с посредником (mediator pattern), который применяется в библиотеке postal.js . В шаблоне с посредником имеется доступный для всех объектов посредник, который ловит и публикует события. При таком подходе один объект не имеет прямых ссылок на другой объект, а следовательно, все объекты отвязаны друг от друга.

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

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

Var doSomethingCoolWithDirections = function(route) { postal.channel("ui").publish("directions.done", { route: route }); };

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

Var UI = function() { this.channel = postal.channel("ui"); this.channel.subscribe("directions.done", this.updateDirections).withContext(this); }; UI.prototype.updateDirections = function(data) { // Маршрут доступен в data.route, теперь нужно просто обновить интерфейс }; app.ui = new UI();

Другие реализации шаблона с посредником используются в библиотеках amplify , PubSubJS и radio.js .

Заключение

JavaScript делает простым процесс создания асинхронных приложений. Использование обещаний, событий или именованных функций позволяет избежать "ада возвратных функций".

Итак, клонируете репозиторий и переходите на ветку Part_1:

Git clone https://github.com/Peleke/promises/ git checkout Part_1-Basics

Вы на пути к истине. Наш маршрут включает в себя следующие вопросы:

  • Проблема функций обратного вызова
  • Промисы: определения и замечания из спецификации
  • Промисы и не-инверсия управления
  • Управление потоком с промисами
  • Осознаем смысл then , reject и resolve
Асинхронность

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

Синхронный и асинхронный код

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

// readfile_sync.js "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "text.txt", fs = require("fs"); console.log("Reading file . . . "); // readFileSync БЛОКИРУЕТ выполнение до возврата значения. // Программа будет ждать и ничего не выполнять, // пока эта операция не завершится. const file = fs.readFileSync(`${__dirname}/${filename}`); // Это ВСЕГДА будет выводится после завершения readFileSync. . . console.log("Done reading file."); // . . . А здесь ВСЕГДА будет выводится содержимое "file". console.log(`Contents: ${file.toString()}`);

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

// readfile_async.js "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "text.txt", fs = require("fs"), getContents = function printContent (file) { try { return file.toString(); } catch (TypeError) { return file; } } console.log("Reading file . . . "); console.log("=".repeat(76)); // readFile выполняется АСИНХРОННО. // Программа продолжит выполнять то, что после LINE A // пока readFile делает свое дело. Мы скоро подробно обсудим // функции обратного вызова, пока просто обратите внимание на порядок логов let file; fs.readFile(`${__dirname}/${filename}`, function (err, contents) { file = contents; console.log(`Uh, actually, now I"m done. Contents are: ${ getContents(file) }`); }); // LINE A // Это ВСЕГДА будет выводится до завершения чтения файла. // Эти логи вводят в заблуждение и бесполезны. console.log(`Done reading file. Contents are: ${getContents(file)}`); console.log("=".repeat(76));

Основное преимущество синхронного кода состоит в том, что его проще читать и воспринимать: синхронные программы выполняются сверху вниз и строка n всегда завершается перед строкой n+1.

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

И именно поэтому JavaScript сделан неблокирующим в своей основе.

Вызов асинхронности

Переход к асинхронности дает нам скорость и забирает у нас линейность. Даже простой скрипт из примера выше демонстрирует это. Запомните:

  • Нет способа узнать, когда file будет доступен, кроме как передав управление readFile , который сообщит нам, когда все будет готово;
  • Наша программа больше не выполняется в том же порядке, в котором читаются, что усложняет их понимание.
  • Этих проблем достаточно, чтобы занять нас до конца статьи.

    Колбэки и фолбэки

    Попробуем немного упростить наш пример с readFile .

    "use strict"; const filename = "throwaway.txt", fs = require("fs"); let file, useless; useless = fs.readFile(`${__dirname}/${filename}`, function callback (error, contents) { file = contents; console.log(`Got it. Contents are: ${contents}`); console.log(`. . . But useless is still ${useless}.`); }); // Thanks to Rava for catching an error in this line. console.log(`File is ${useless}, but that"ll change soon.`);

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

    Вопрос в том, как мы можем узнать, что чтение завершено?

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

    Работает это примерно так: readFile смотрит,что находится внутри ${__dirname}/${filename} , а программа занимается своими делами. Как только readFile узнает, что там, он выполняет callback с contents в качестве аргумента, а в случае ошибки возвращает error .

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

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

    Функции обратного вызова это работающее решение, но не идеальное. У них есть две большие проблемы:

  • Инверсия управления
  • Сложная обработка ошибок
  • Инверсия контроля

    Первая проблема это проблема доверия.

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

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

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

    Неявная обработка ошибок

    В синхронном коде вы можете использовать try / catch / finally для обработки ошибок.

    "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "text.txt", fs = require("fs"); console.log("Reading file . . . "); let file; try { // Неправильное имя файла. D"oh! file = fs.readFileSync(`${__dirname}/${filename + "a"}`); console.log(`Got it. Contents are: "${file}"`); } catch (err) { console.log(`There was a/n ${err}: file is ${file}`); } console.log("Catching errors, like a bo$$.");

    Асинхронный код пытается, конечно, но…

    "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "throwaway.txt", fs = require("fs"); console.log("Reading file . . . "); let file; try { // Неправильное имя файла. D"oh! fs.readFile(`${__dirname}/${filename + "a"}`, function (err, contents) { file = contents; }); // Это не будет выполняться пока file равен undefined console.log(`Got it. Contents are: "${file}"`); } catch (err) { // В этом случае, catch должен запускаться, но это никогда не произойдет. // Это потому, что readFile передает ошибки в коллбэк, // а не возвращает. console.log(`There was a/n ${err}: file is ${file}`); }

    Это не работает так, как ожидается. Потому что блок try оборачивает readFile , который всегда успешно возвращает undefined . В такой ситуации у try всегда будет без происшествий.

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

    "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "throwaway.txt", fs = require("fs"); console.log("Reading file . . . "); fs.readFile(`${__dirname}/${filename + "a"}`, function (err, contents) { if (err) { // catch console.log(`There was a/n ${err}.`); } else { // try console.log(`Got it. File contents are: "${file}"`); } });

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

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

    Промисы

    Представьте, вы только что заказали весь каталог You Don’t Know JS от O’Reilly. За ваши с трудом заработанные деньги они прислали расписку, что в следующий понедельник вы получите новенькую стопку книг. До этого счастливого понедельника никаких книг у вас не будет - но вы верите, что они появятся, так как вам пообещали (promise) прислать их.

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

    Конечно, через несколько дней O’Reilly может сообщить о том, что с понедельником не судьба и книги будут чуток позже, другими словами, нужное значение будет в будущем. Вы относитесь к промису, как к ожидаемому значению и пишете код так, как будто оно уже у вас есть.

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

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

    Жизненный цикл промиса: краткий обзор состояний

    Представьте, что вы используете промис для вызова API.

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

    Как только сервер ответил, у нас есть два возможных исхода:

  • Промис получает ожидаемое значение, значит, он выполнен (fulfilled) . Ваши книжки пришли.
  • Где-то по ходу выполнения произошла ошибка, промис отклонен (rejected) . Вы получили уведомление о том, что никаких книжек не будет.
  • Всего мы получаем три возможных состояния промиса , при этом состояния выполнения или отклонения не могут смениться другим состоянием.

    Теперь, когда мы разобрались с основными понятиями, посмотрим, как это все использовать.

    Фундаментальные методы промисов

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

    В этом разделе мы ближе рассмотрим базовое использование промисов:

  • Создание промисов с конструктором;
  • Обработка успешного результата с resolve ;
  • Обработка ошибок с reject ;
  • Настройка управления потоком с then и catch .
  • В нашем примере мы будем использовать промисы для очистки кода нашей функции fs.readFile .

    Создание промисов

    Самый простой способ это создание промисов непосредственно с помощью конструктора.

    "use strict"; const fs = require("fs"); const text = new Promise(function (resolve, reject) { // Does nothing })

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

  • Аргумент resolve это функция, инкапсулирующая то, что мы хотим сделать при получении ожидаемого значения . Когда мы получаем ожидаемое значение (val), мы передаем его resolve в качестве аргумента: resolve(val) .
  • Аргумент reject это тоже функция, представляющая наши действия в случае получения ошибки. Если мы получим ошибку (err), мы вызовем reject с ней: reject(err) .
  • Наконец, функция, переданная нами в конструктор промиса, обрабатывает сам асинхронный код. Если она возвращает ожидаемый результат, мы вызываем resolve с полученным значением. Если она выбрасывает ошибку, мы вызываем reject с этой ошибкой.
  • В нашем примере мы обернем fs.readFile в промис. Как должны выглядеть наши resolve и reject ?

  • При успехе мы хотим вызвать console.log для вывода содержимого файла.
  • При неудаче мы поступим аналогично: выведем ошибку в консоль.
  • Таким образом мы получим следующее:

    // constructor.js const resolve = console.log, reject = console.log;

    Затем нам надо написать функцию, которую мы передаем конструктору. Запомните, нам нужно сделать следующее:

  • Прочитать файл;
  • В случае успеха выполнить resolve с его содержимым;
  • При неудаче выполнить reject с полученной ошибкой.
  • Таким образом:

    // constructor.js const text = new Promise(function (resolve, reject) { // Normal fs.readFile call, but inside Promise constructor . . . fs.readFile("text.txt", function (err, text) { // . . . Call reject if there"s an error . . . if (err) reject(err); // . . . And call resolve otherwise. else // fs.readFile возвращает buffer, поэтому надо применить метод toString(). resolve(text.toString()); }) })

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

    Она дала обещание, а затем…

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

    Каждый промис обладает методом then , принимающим две функции в качестве аргументов: resolve и reject , именно в таком порядке. Вызов then в промисе и передача ему этих двух функций, делает их доступными для конструктора промиса.

    // constructor.js const text = new Promise(function (resolve, reject) { fs.readFile("text.txt", function (err, text) { if (err) reject(err); else resolve(text.toString()); }) }) .then(resolve, reject);

    Так промис прочитает файл и вызовет написанный нами метод resolve в случае успеха.

    Важно запомнить, что then всегда возвращает объект-промис . Это значит, что вы можете сделать цепочку из нескольких вызовов then для создания сложного и синхронно-выглядящего потока над асинхронными операциями. В следующей статье мы обсудим это подробнее, а как это выглядит, мы поймем разбирая пример catch .

    Синтаксический сахар для обработки ошибок

    Мы передали then две функции: resolve , для вызова в случае успеха и reject на случай ошибки.

    У промисов также есть функция похожая на then , называемая catch . Она принимает обработчик reject в качестве единственного аргумента.

    Так как then всегда возвращает промис, в нашем примере мы можем только передать then обработчик resolve , а после этого подключить в цепочку catch с обработчиком reject .

    Const text = new Promise(function (resolve, reject) { fs.readFile("tex.txt", function (err, text) { if (err) reject(err); else resolve(text.toString()); }) }) .then(resolve) .catch(reject);

    Наконец, стоит упомянуть, что catch(reject) это всего лишь синтаксический сахар для then(undefined, reject) . То есть мы можем также написать:

    Const text = new Promise(function (resolve, reject) { fs.readFile("tex.txt", function (err, text) { if (err) reject(err); else resolve(text.toString()); }) }) .then(resolve) .then(undefined, reject);

    Но такой код будет менее читаемым.

    Заключение

    Промисы это незаменимый инструмент для асинхронного программирования. Они могут напугать поначалу, но только пока вы с ними незнакомы: используйте их пару раз и они станут такими же естественными для вас как if / else .

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

    В качестве дополнительной литературы ознакомьтесь со статьей Доменика Дениколы States and Fates , чтобы овладеть терминологией и с главой Кайла Симпсона о промисах из той стопки книг, на примере которой мы разбирали промисы.