» » Разъяснительная беседа об асинхронном программировании в Javascript

 

Разъяснительная беседа об асинхронном программировании в Javascript

Автор: admin от 25-12-2018, 18:10, посмотрело: 39

Привет всем!



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

Шаблон



Решение



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



Но ситуация усугубляется, не правда ли? Реализация машины состояний все сильнее отдаляется от реализации генератора. Мало того, что из-за обработки ошибок код обрастает мусором; код тем более усложняется из-за такого длинного цикла while, который здесь у нас получился. Для преобразования цикла while его нужно «расплести» в состояния. Так, наш случай 1 фактически включает 2,5 итерации цикла while, поскольку yield обрывается на середине. Наконец, приходится добавить лишний код для продвижения исключений от вызывающей стороны и обратно, если в генераторе не найдется блока try/catch для обработки этого исключения.



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




  • Генератор может порождать значения, потреблять значения, либо и то, и другое.

  • Состояние генератора можно ставить на паузу (состояние, машина состояний, улавливаете?)

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

  • Исключения пересылаются в любом направлении.



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



Инверсия управления при помощи корутин



Теперь, поднаторев в работе с генераторами, давайте подумаем, как их можно применять при асинхронном программировании. Если мы умеем писать генераторы как таковые, это еще не означает, что промисы в генераторах автоматически будут разрешаться. Но, подождите, генераторы и не предназначены работать сами по себе. Они должны взаимодействовать с другой программой, основной процедурой, той, что вызывает .next() и .throw().



Что, если помещать нашу бизнес-логику не в основную процедуру, а именно в генератор? Всякий раз, когда бизнес-логике попадется некоторое асинхронное значение, скажем, промис, генератор сообщит: «не хочу возиться с этой дурью, разбудите меня, когда она разрешится», приостановится и выдаст промис обслуживающей процедуре. Обслуживающая процедура: «хорошо, попозже тебя позову». После чего она регистрирует обратный вызов с этим промисом, выходит и дожидается, пока можно будет вызвать цикл событий (то есть, когда промис разрешится). Когда это произойдет, процедура возвестит: «эй, твоя очередь», и отправит значение через .next() спящему генератору. Будет ждать, пока генератор сделает свое дело, а сама тем временем займется другими асинхронными делами… и так далее. Вы прослушали грустную историю о том, как живется процедуре на услужении у генератора.



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



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



co(function* () {
  var user = yield fetchJson('/api/user/self');
  var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = yield Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
});


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



Задача на программирование — co() простая



Отлично! Теперь давайте сами соберем co(), чтобы интуитивно понять, как именно работает такая вспомогательная процедура. co() должна




  1. Возвращать промис вызывающей стороне, которая его ждет

  2. Инстанцировать генератор

  3. Вызывать .next() в генераторе для получения первого выданного результата, который должен иметь вид {done: false, value: [a Promise]}

  4. Зарегистрировать обратный вызов с промисом

  5. Когда промис разрешится (будет сделан обратный вызов), вызвать .next() в генераторе, с разрешившимся значением и получить обратно другое значение

  6. Повторить все, начиная с шага 4

  7. Если в какой-то момент генератор вернет {done: true, value: ...}, разрешить промис, возвращенный co()



Пока давайте не задумываться об ошибках, напишем простой метод co(), позволяющий обработать приведенный ниже искусственный пример:



Шаблон



function deferred(val) {
  return new Promise((resolve, reject) => resolve(val));
}

co(function* asyncAdds(initialValue) {
  console.log(yield deferred(initialValue + 1));
  console.log(yield deferred(initialValue + 2));
  console.log(yield deferred(initialValue + 3));
});

function co(generator) {
  return new Promise((resolve, reject) => {
    // Ваш код
  });
}


Решение



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



Задача на программирование – обработка исключений в co()



Когда промис, выданный генератором, отклонен, мы хотим, чтобы co() сигнализировала процедуре генератора об исключении. Как вы помните, в интерфейсе генератора предоставляется метод .throw() для отправки исключений.



Шаблон



function deferred(val) {
  return new Promise((resolve, reject) => resolve(val));
}

function deferReject(e) {
  return new Promise((resolve, reject) => reject(e));
}

co(function* asyncAdds() {
  console.log(yield deferred(1));
  try {
    console.log(yield deferredError(new Error('To fail, or to not fail.')));
  } catch (e) {
    console.log('To not fail!');
  }
  console.log(yield deferred(3));
});

function co(generator) {
  return new Promise((resolve, reject) => {
    // Ваш код
  });
}


Решение



Тут все немного усложняется. Нам понадобятся разные обратные вызовы в зависимости от того, разрешен был выданный промис или отклонен, поэтому в решении следующий вызов .next() выносится в отдельный метод onResolve(). Также здесь используется отдельный метод onReject(), который при необходимости будет вызывать .throw(). Оба этих обратных вызова обернуты в блоки try/catch каждый, чтобы сразу же отклонять промис, если в генераторе не предусмотрен try/catch на случай ошибок.



Итак, мы построили co()! Почти! co() также поддерживает трамплинные функции, вложенные генераторы, массивы из вышеперечисленного, а также глубокие объекты. Но волшебства почти не осталось, правда?



Священный грааль: async/await



Вот мы и разобрались с генераторами и с co(). Но есть ли в них какой-нибудь прок, если в нашем распоряжении будет async/await? Ответ — ДА! Поскольку мы со всеми ними уже разобрались, нам не составит труда понять и async await.



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



Итак, чтобы наша функция использовала async/await, а не генераторы, всего-то и нужно заменить co() на async и yield на await, а также убрать из функции *, чтобы она перестала быть генератором.



co(function* () {
  var user = yield fetchJson('/api/user/self');
  var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = yield Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
});


Становится:



async function () {
  var user = await fetchJson('/api/user/self');
  var interests = await fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = await Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
}();


Однако, здесь нужно отметить пару небольших особенностей:




  • co() сразу же выполняет асинхронный генератор. async создает функцию, но ее вам все равно еще нужно вызвать. async больше напоминает вариант co() под названием co.wrap().

  • С co() можно выдавать (yield) промисы, трамплинные функции, массивы промисов или объекты промисов. С async можно только ожидать (await) промисы.



Конец



Мы рассмотрели историю асинхронного программирования в javascript с некоторыми сокращениями, разобрались, как «за кулисами» устроена работа генераторов и co(), а затем, опираясь на изученный материал, освоили работу с async/await. Гордитесь? Правильно.

Источник: Хабр / Интересные публикации

Категория: Программирование, Веб-разработка

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *