Комментарии 21
На $mol это было бы как-то так, если дословно переводить:
const getA = sync( ()=> {
const a = sync( api ).getA()
return a
} )
const getB = sync( params => {
const b = sync( api ).getB( params )
return b
} )
const event = task( () => {
const a = getA()
const b = getB(a)
setState( b )
} )
И так, если нормально делать:
class App extends Object {
@mem api() {
return new Api
}
@act getA() {
const a = this.api().getA()
return a
}
@act getB( params ) {
const b = this.api().getB( params )
return b
}
@mem state() {
const a = this.getA()
const b = this.getB(a)
return b
}
}
Неделю назад написал эту штуку для своей библиотеки @cmmn/cell
, но еще не добавил в либу
Выглядит так:
const a = new CellQuery<ResultA>('/api/a');
const b = new CellQuery<ResultB>(() => a.isFetching ? undefined : `/api/a/${a.data.id}`);
и в реакте
const {isFetching, data} = useCell(b);
return isFetching ? <Skeleton/> : <div>{data}</div>;
CellQuery чекает зависимости и вызывает fetch если что-то изменилось. Если уже идет запрос, он отменяется.
Хорошая статья. Хорошо, когда в проекте это реализовано изначально и все пишут с таким подходом на их стеке технологий проекта.
Плохо, когда этого совсем нет)
Повторюсь, важное отличие реализации отмены в Reatom от rxjs и
redux-saga является в использовании нативного AbortController, который
уже является стандартом, используется в браузерах и node.js, а также
множества других библиотек
Все же он используется не напрямую, а внутри withAbort() абстракции. Что тоже "специфический АПИ". А завязка внутри на AbortController - наоборот делает реализацию менее универсальной. Например, в rxjs - unsubscribe можно написать для любой асинхронной операции, даже если она не поддерживает AbortController. К примеру, XmlHttpRequest.
Rxjs придумали в 1875.
Люди до 1875 - как отменять цепочки асинхронных событий???
Уважаемые фронтовики (или передовики, как правильно?)!
Как задовик, я понимаю ваши проблемы и даже немного сочувствую. Но — не разделяю.
Например, я в примере (точнее, на диаграмме) вижу нарушение принципа "дураку полработы не показывают": ну вот зачем после getA надо показывать промежуточные результаты пользователю (которого чисто для пользы дела следует считать дураком)? Лучше было бы дождаться getB, собрать всю полученную инфорацию в кучу и сразу обновить отображаемые данные. Ведь в коде примера, на самом деле, оно так и сделано, если я правильно понял смысл заклинания setState. Версионирование при таком подходе работает на ура — версию достаточно проверить один раз, перед обновлением отображаемых пользователю данных. В примере, насколько я понял это стоит сделать в setState. Благо, вы там на фронте можете себе позволить дождаться конца каждой цепочки, не экономя на запросах к серверу — задовики как-нибудь справятся, да ;-).
Во-вторых, я рад, что до фронта наконец-то добрался шаблон скоординированной отмены (который AbortController, в C# это — CancellationToken и все что вокруг него). Вот проблемы с прокидыванием его я как раз понимаю — у нас так же мучаться приходится. И решение примерно то же самое — контекст. И даже для прокидывания контекста ничего особо изобретать не нужно — для этого есть объекты и ключевое слово this в их методах. У вас на фронте тоже так же можно, только чуть побольше "бойлерплейта" надо — например, писать "function" вместо "=>" (ну, и список параметров переставить по месту). Как по-моему, то лишние 6 символов на функцию того стоят. Может, попробуете? В комменте nin-jin примерно показано, как это делается (подсказка: его любимый $mol для этого, в общем-то, не нужен).
А вот чего, по-моему, делать не стоит — так это использовать для хранения контекста глобальную переменую — даже которая на уровне модуля. Ибо это — костыль жуткий: что вы делать-то будете, когда вам потребуется параллельно выполнять две цепочки? Или это — чисто для примера? Так для примера я бы блок не поленился нарисовать — у вас же модуль и там всегда "use strict", так что всего два символа — и любой приверженец методологии разработки SO-Driven Development будет в безопасности (относительной) ;-)
А что по поводу Reatom, то я к лишней зависмости всегда отношусь с подозрением (я не люблю зависимости, хотя, допускаю, что я просто не умею их готовить). Потому пофантазирую: нельзя ли то же самое сделать через Proxy в чисто конкретных местах?
PS Надеюсь, то что я тут говорю тут про задачи (Task/promise) а не про async/await это никого не смущает? Если вдруг смущает — могу пояснить.
PPS Немного побуду Шишковым: "to track" переводится с английского (в данном контексте) как "отслеживать". Так что, можно вместо изобретения своих словоуродцев использовать его. Впрочем, лично для меня это не страшно — читать русские тексты по IT методом обратного перевода я привык со времен ЕС ЭВМ — но я за других беспокоюсь.
PPS Если чо не то сказал — извиняйте: ну, не фронтовик я (и не передовик).
А как это делается без $mol?
Что-то типа такого (основываюсь на вашем примере из второго листинга, а так как JS — не мой родной язык, то если что не так — подправьте)
{
class App {
constructor(controller_name) {
this.сontrollerName = controller_name;
this.controller = new AbortController();
}
async event(getA, getB) {
if(this.controller) controller.abort(controllerName);
this.controller = new AbortController();
const a = await this.getA();
const b = await this.GetB(a);
// controller.throwIfAborted() добавить по вкусу
return b;
}
}
async function getA() {
let a;
//...do smth
this.controller.throwIfAborted();
//...do smth more
//при необходимости повторить предыдущие две строчки до готовности
return a
}
async function getB(a) {
let b;
//...do smth with a
this.controller.throwIfAborted();
//...do smth more with a
//при необходимости повторить предыдущие две строчки до готовности
return b
}
}
У вас даже близко не то же самое: функции другого цвета, куча бойлерплейта, ручное прокидывание аборт контроллера и прерывание функций, ещё и апи потерялось.
Дык вопрос-то был, как это сделать без этого вашего $mol в принципе, а не как это сделать красиво. Кароче, вы мне тут напишите — код работать будет вообще, в принципе? Если да, то мне для иллюстрации достаточно.
PS Api — это DI-котейнер, или что? Если DI-котейнер, то Api не нужен потому что я написал PoC: нужные функции передаются напрямую, как параметры. Впихнуть DI-котейнер несложно, только вот он число сущностей умножит, а я старался упростить.
И да, getA и getB просто не читайте: они тут чисто для иллюстрации, кужа скоординированную отмену впихивать, дабы не гадал никто.
api - фасад к серверу, который собственно и выполняет запросы.
Это — свой объект для каждой цепочки запросов? Тогда вот вам и готовый контекст, куда можно AbortController засунуть. Ну и, чтобы сделать такой, какой-то особый фреймворк не нужен — достаточно создать объект и прописать в нем нужные методы. Вы согласны?
Общий для всех.
Хотя… Если прикладные программы всегда получают его через new Api, как у вас в примере, то можно совершенно прозрачно поменять функцию Api так, чтобы она возвращала новый объект, унаследованный от основного объекта Api, но со своим собственным AbortController.
Как получают не важно, api - один объект, который реализует много разных методов.
Как получают — важно, потому что если это — глобальный объект, который используют все, то предыдущий варинат не пройдет.
И если один объект реализует много разных методов, то и объект, созданный на на основе него как прототипа, будет реализовать те же самые методы, в принципе (детали возможны, да).
Привет, спасибо за такой подробный комментарий, но вы многое упускаете :)
"зачем показывать промежуточные результаты?"
Мы всегда стараемся показать данные как можно быстрее в хорошем интерфейсе. Простым решением проблемы, и как многие делают, было бы просто перекрытие всего интерфейса спинером, пока все запросы не завершаться, как оно там под капотом обновляется - не важно. Но это плохой интерфейс сразу по нескольким причинам.
"версию достаточно проверить один раз"
В этом примере - да, но встречаются более сложные примеры с несколькими отдельными визуально, но связанными логически интерфейсами, где следить ручками за версиями будет сложнее и может породить лишние баги. Я предлагаю универсальное решение.
"контекст"
Это общее слово и тут нужно понимать про какой контекст мы говорим, в каком контексте :) Есть контекст модели - инстанса. Есть асинхронный контекст вызова - процедуры. Абсолютно разные вещи.
Для асинхронного контекста сама переменная "context" - просто токен, данные по которому меняются и для каждой асинхронной цепочки свои.
"лишней зависмости"
В жаваскрипте плохой STD и вообще нет фреймворков, вы либо пишите вагоны велосипедов, либо пишите говнокод без хороших практик. Отсутствие библиотек - очень тревожный звоночек. У библиотек документация и тесты будут точно лучше внутренних реализаций, у нас такая культура во фронте. Кстати, бандлсайз Reatom очень маленький: 2кб core и еще 2-4кб на async и остальные вспомогательные либы. Сам React весит 44.
Привет, спасибо за такой подробный комментарий, но вы многое упускаете :)«Нельзя объять необъятное» (Козьма Прутков). Я всего лишь демонстрирую взгляд с другой стороны.
Мы всегда стараемся показать данные как можно быстрее в хорошем интерфейсе.А эти данные в таком неполном виде точно для пользователя какую-то ценность имеют? Если да, и если данные отображаются в нескольких частях (типа основной записи и детализации или наоборот, каких-то суммарных значение), то ваш варинт — обновление интерфейса несколько раз в процессе запроса — имеет смысл, и однократной проверкой номера версии тогда не обойтись. Но в примере — в коде, а не на диаграмме- обновление интерфейса было одно, и я на это обратил внимание.
Тем не менее, идея с версией обновления мне лично не кажется особо хорошей, шаблон скоординированной отмены мне нравится больше. Может, потому что привык — он в C# уже больше 10 лет существует.
встречаются более сложные примеры с несколькими отдельными визуально, но связанными логически интерфейсами,
Мое мнение — делать обновление таких частей методом(ами) одного объекта, который и будет контекстом для всей цепочки. Благо JS позволяет много чего лишнего, чего лишены разработчики на языках со статической типизацией ;-), в данном случае — заимствование методов из других объектов.
нужно понимать про какой контекст мы говорим, в каком контексте
В контексте здешнего обсуждения контекст это исключительно про набор взаимосвязанных асинхронно выполняющихся запросов к серверу. Контекст приложения оставляем в стороне, как и само приложение, каково бы оно ни было (а они ведь разные бывают, не так ли?).
у нас такая культура во фронтеДык, я в чужой монастырь со своим уставом лезть и не собираюсь. Нелюбовь к зависимостям — это мое личное предпочтение. А насчет библиотек — которые не от лидеров рынка, а от таких же разработчиков — я бы не был столь оптимистичен Хотя, возможно, у вас там на фронте с кодом приложений ещё хуже, не знаю.
Всегда радуют такие переходы: проблему решили, но код некрасивый, а есть ещё другие какие-то сложные случаи - давайте подключать библиотеку. Причем не просто утилиту, а стэйт-менеджер, легко же все переписать)
Пока нативного AsyncContext нет - все так :)
Хотя и с ним проблем достаточно останется. Если писать только на нативке, без библиотек, у вас либо будет чрезмерно упрощенный и не надежный код, либо собственные библиотеки. Ну либо задачи решаете очень простые, конечно.
Хотя я вот сколько мелких одностраничных лендингов / приложений не писал, все равно для хорошего UX нужно либо написать кучу кода, либо тащить тяжелые библиотеки. Мой опыт. Поэтому Reatom и пишу, пытаюсь решить основные юзкейсы за бандлсайз, который не страшно тащить даже на леднос.
Отменить нельзя продолжить