Объяснение использования кортежей в поисках гибкой асинхронной разработки.
На протяжении всего развития 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. В зависимости от вашего уровня приверженности специфичности они могут быть или не быть важными для вас. Я видел большое количество аргументов в Интернете по этому вопросу, но это выходит за рамки этой статьи: лично я предпочитаю быть очень конкретным по нескольким причинам:
- Большой PR / более поздний ввод кода становится более быстрым для других членов команды. Ожидания от переменного поведения определены для вас явно.
- Личный опыт: ошибка переназначения 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 — это самая большая боль, которую ваши глаза когда-либо испытывали в кодовой базе.