Объяснение использования кортежей в поисках гибкой асинхронной разработки.

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

Вначале были обратные вызовы, которые привели к общеизвестным сценариям типа ад обратных вызовов. Да, существовали различные способы обеспечения более чистых подходов, но, в конце концов, это все еще было уродливо, сложно поддерживать для больших команд и в целом приводило к плохому опыту разработчиков. Я мог бы привести примеры, но на самом деле нет никакой положительной причины для публикации этого. Я надеюсь, что каждый сценарий использования для обратных вызовов будет отменен, как какая-нибудь Карен 2020 года после расистской тирады в местном парке. Если вы все еще чувствуете потребность в объяснении, проверьте здесь.

Введите ECMAScript 2015: (он же: ECMAScript 6, он же: ES6). Вместе с ES6 появились некоторые основные компоненты, которые будут использоваться в приведенных ниже пояснениях.

  • Javascript обещает стандартизацию
  • пусть/постоянные ключевые слова
  • Назначение деструктурирования

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

function fetchExternalDataAndPersist() {
 fetch('some_url')
    .then(data => {
      // now I can work on my data.
      // now I need to persist it to a database
      // hopefully provides a promise interface over a callback one
      SomeDatabaseInterface.writeSomething(data)
                 .then(data => {
                   // maybe I need to do something else now.
                   // like even make another promise call
                   // you see where this is going
                 })
                 .catch(error => {
      // Oh noes, I've got an error
      // now I need to either define all error handling here
      // or pass all of my relative lexically scoped data
      // which could be a lot depending on the level of nesting
                 })
                 .finally(() => {
                   // shut off db connection if required
                   // or maybe do some write type based callback
                 })
    })
    .catch(err => {
      // must define a callback for error handling
    })
    .finally(() => {
      // only valuable if you are not interested in the
      // returned data or error
    })
}

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

function fetchExternalData() {
 return fetch('some_url')
         .then(...doSomethingAndReturnIt)
         .catch(...handleThisError...somewhereElse?);
}
function persist(data) {
 return SomeDatabaseInterface.writeSomething(data)
        .then(...returnSomething)
        .catch(...handleThisError);
}
function fetchExternalDataAndPersist() {
 fetchExternalData()
  .then(persist)
  .then(...)
  .catch(...)
}

Но в конце пути у вас всегда были какие-то вложенные then-ables, что означало, что вам всегда приходилось передавать какие-то данные с отслеживанием состояния для обработки ошибок.

ES6 также добавил ключевые слова let/const. В зависимости от вашего уровня приверженности специфичности они могут быть или не быть важными для вас. Я видел большое количество аргументов в Интернете по этому вопросу, но это выходит за рамки этой статьи: лично я предпочитаю быть очень конкретным по нескольким причинам:

  1. Большой PR / более поздний ввод кода становится более быстрым для других членов команды. Ожидания от переменного поведения определены для вас явно.
  2. Личный опыт: ошибка переназначения const несколько раз спасала меня от самого себя.

Присвоение деструктурирования: это добавило хороший чистый способ доступа к известным значениям, извлеченным из хранилищ ключей/значений и массивов.

const myReturn = {
  err: 'some error',
  data: [1, 2, 3]
};
const { err, data } = myReturn;
const [firstData] = data;

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

После того, как у нас появились промисы, деструктуризация и let/const, пролетели несколько лет, и в сообществе появился общий шаблон упаковки/распаковки промисов, что вызвало потребность в более общем подходе к проблеме. Введите ECMAScript 2017 async/awaits. Эти новые ключевые слова предоставили способ автоматически оборачивать и разворачивать промисы, так что не требовалось «тогда/поймать/наконец», что формально завершало шаблон обратного вызова. Но подождите… что это за УЖАСНЫЙ побочный эффект / поломка лексической области видимости, которую я вижу!?

В этом примере, что произойдет, если наша выборка не удастся? Поведение async-awaits по умолчанию приводит к сбою «вверх», полностью выходя из функции и не запуская какой-либо последующий код. Иногда это может быть желательно, но, по моему опыту, это скорее исключение, чем правило требования. Я очень надеюсь, что здесь не было никаких значений с лексической областью видимости, которые каким-то образом должны быть переданы «обратно» при возникновении ошибки…

async function fetchSomething() {
  const val = await fetch('some_url');
  // use the value to craft something
  const computed = {...val, fetchedBy: user.id }
  return computed;
}

Чтобы решить вышеуказанную проблему, распространенным вариантом использования становится стандартная упаковка try-catch.

async function fetchSomething() {
  let val;
  let err;
  
  try {
    val = await fetch('some_url);
  } catch(e) {
     err = e;
  }
  if(err) {
   return { err, fetchedBy: user.id };
  }
  return {...val, fetchedBy: user.id }
}

Давайте поговорим об этом… потому что хороший способ справиться с травмой — это поговорить о ней. Этот метод просто полностью уничтожил возможность использования const, создал отвратительный шаблон try-catch и вынудил нас вручную устанавливать ошибки в потоке управления. Хотя это очень наивный пример, не стоит недооценивать негативные последствия удаления функциональности из языка. Что, если я скажу тебе, что есть способ не только съесть свой торт, но и съесть его?

Введите набор кортежей возвращаемых значений с помощью библиотеки ожидание до. При этом используется деструктуризация массива и async/await, что позволяет кодовой базе постоянно поддерживать лексическую область видимости и использовать полный набор ключевых слов let/const.

Давайте попробуем переписать вышеприведенное с коллекцией кортежей.

const { s } = require('awaits-until');
async fetchSomething() {
  const [err, data] = await s(fetch('some_url'));
  if(err) {
    return { err, fetchedBy: user.id };
  }
  return {...val, fetchedBy: user.id };
}

Теперь мы можем установить наш const один раз, не нужно предварительно объявлять переменные и не нужно беспокоиться о «сбое вверх» при отклонении. Это не какое-то дополнение к языку, меняющее игру, но это более чистый метод обработки промисов с помощью await/async, и вариант использования становится тем более ценным, чем больше логика зависит от отдельных возвратов промисов. IE: интеграционный микросервис, который использует API или массовые записи отдельных БД (например, DynamoDB).

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