Pull to refresh

Comments 50

А мне обычно достаточно промисов, а Rx считаю излишним усложнением. Ок, нельзя отменять, но зачем? Чаще всего результат идёт в локальный стейт компонента, ну и пусть пишет сколько хочет. Дебоунс нужен, но он просто вешается на внешнюю функцию/метод и все. А остальные Rx операторы если более менее серьезно использовать превращаются в кашу, которую ещё надо постараться отдалить. Не против Rx в целом, но против его пиара как универсального средства на замену промисам. Потому что ангуляр решил выпендриться. Почему observable? Давайте сразу каналы и возможность напихивать с двух сторон, но кому это надо?
На мой взгляд лучше взять простейшие промисы, async/await и допилить где надо, чем сразу брать Rx, потому что он как бы лишён недостатков промисов. Не надо забывать что главная проблема разработки — это сложность ПО, а ее недостаток фич. Ну я про свой CRUD все конечно, может у кого проблемы поглобальнее.
Спасибо за ваш комментарий. Где ж он был когда я стирм вел, а то там все такие стесняшки были)

Согласен, что для простых проектов, когда у вас обычный запрос/ответ Rx избыточен. Да и технологии выбираются из расчета задач и компетенции команды.

А в целом RxJs хорош. Подписку не сделал — поток не работает, память не кушает; для работы с данными можно использовать единый интерфейс будь то клиентское событие или web-socket; связь один-к-многим вообще огонь, когда ты создаешь свой горячий поток.

Как же мне понятны ваши аргументы и как же вы неправы :) Был на вашем месте пару лет назад, когда начинал работать с Ангуляром. Считал observable каким-то выпендрежем без цели. Сейчас мне больно смотреть на спагетти с async/await.
Почему observable?

Потому что выбор правильных абстракций — это половина успеха, а весь наш мир — это один большой поток событий, который движется только вперед (кстати, поэтому двусторонний канал уже излишество) и именно поэтому CQRS шагает по планете вместе с функциональным программированием.
Чтобы прочувствовать rxjs попробуйте написать свой triple/quad click в качестве упражнения классически на таймаутах, а потом с rxjs. Я даю такое задание своим джуниорам. Тут можете подсмотреть ответ. У меня получилось уложиться в 5 строчек.
А чтобы его полюбить, напишите свой хороший typeahead с отменой HTTP запросов на промисах и с rxjs, почувствуйте разницу.
Не уверен что вы поняли мой посыл. Абстракция на то и абстракция, что берет только необходимое из реального мира. И в моем случае, также как мне кажется в большинстве, как минимум на данный момент то что я вижу на рынке и скорее всего останется — промисов вполне достаточно. А значит и не нужно пенеусложнять, придумывая себе проблемы на пустом месте. Я это про себя, а не про вас. Возможно у вас такие задачи составляют основную часть, но у меня — это такой минимум, что мне проще завернуть один хелпер, чем тащить Rx, потому что это однозначно усложнение, лишние абстракции, лишний вес, и пока нет нативной поддержки. Так что нет абсолют правильных абстракций, есть уместные. Таймауты прекрасно оборачиваются в промисы и я не понял о каком спагетти идёт речь, прекрасный линейный код на async/await.
Я вам дал несколько простых пирмеров, где промисов уже недостаточно. Неужели вы никогда не писали свой typeahead? Или, например, 3 зависимых селекта Страна -> Регион -> Город. На каждом шаге нужно сделать запрос, чтобы загрузить список регионов и городов. Если меняем страну нужно сбросить регион и город. Поверх этого неплохо было бы еще навернуть нормальный retry policy. При ошибке запроса делаем еще один, потом через 500мс, потом 2000мс, потом кидаем ошибку. Хотел бы я посмотреть, как это смотрится без реактивности.
Выберите один и разберём если хотите. С вас реализация и с меня. Хотя скорее всего это уже миллион раз было написано, может проще поискать? То что промисы не умеют это из коробки, не значит что нельзя вообще. Правильно? Вопрос в сложности реализации. Ок, можем померить. Только как будем оценивать?
Что касается retry policy ну такое себе. Даже если оно и есть, то уж наверно в одном месте завернуто и по большому счету какая разница как оно реализовано.
Мне кажется вы упорно пытаетесь игнорировать мой аргумент, что всему свое место. Допустим даже все ваши примеры будут в реальном приложении. Это ещё совсем не говорит о том, что теперь нужен rx. В этих случаях возможно красиво, но как насчёт всего остального? Концепция промисов проста и линейна, в ней проще разобраться, тогда как в Rx есть миллион операторов, которые ещё надо найти/запомнить. В целом то код усложнится. Вопрос ради чего? Ради красоты в трёх местах? Ок, у вас таких мест много — берите Rx, я бы и сам взял. Но это далеко не мейнстрим как мне кажется. И да, потом ещё отлаживать этот винегрет.
То что промисы не умеют это из коробки, не значит что нельзя вообщ

Если есть что-то «из коробки», то тогда лучше его выбрать, чем городить очередной свой велосипед, с которым другим людям нужно будет разбираться. Когда приходишь на новый проект и видишь применение стандартных механизмов предназначенных именно для решения конкретных проблем, как-то лучше работается, чем очередной раз смотреть «на полет мысли предыдущего автора».
Это элементарно сделать, но боюсь представить какое это убожество будет с Rx
Сейчас мне больно смотреть на спагетти с async/await.

В случае async/await поток исполнения линеен, так что он не является спагетти по определению. В отличие от нагромождения замыканий в случае rxjs.


весь наш мир — это один большой поток событий

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


поток событий, который движется только вперед (кстати, поэтому двусторонний канал уже излишество)

Вы получили документ по ссылке http://example.org/foo/bar. По какой ссылке нужно его сохранять, чтобы не нарваться на двусторонний канал?


  • http://example.org/foo/bar?write — отдельный канал для записи ресурса, но роутер направит запрос на один и тот же обработчик, который получается будет двусторонним каналом.
  • http://example.org/write?foo/bar — разные обработчики, но сервер-то один. Опять двусторонний канал.
  • http://write.example.org/foo/bar — разные сервера, но протокол-то единый, выступающий как двусторонний канал в интернет.
  • http-write://example.org/foo/bar — разные протоколы, но через одну точку доступа, которая выступает двусторонним каналом связи с сетью.

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


Это всё к вопросу о "правильных" абстракциях, соответствующих "всему нашему миру".


Я даю такое задание своим джуниорам. Тут можете подсмотреть ответ. У меня получилось уложиться в 5 строчек.

К сожалению, вы ни нативный код написать нормально не смогли, ни, что забавно, RXJS. Я поправил: https://stackblitz.com/edit/rxjs-bxmh4n?file=index.ts


Обратите внимание на share, чтобы не было 100500 подписок в доме. На копипасту, чтобы повесить разные обработчики на разное число кликов. На потерю и восстановление объекта события. На обработку дефолтной ветки логики. И, конечно, на то, что кода на RxJS получилось в полтора раза больше. Причём куда более сложного, чем нативный.


А чтобы его полюбить, напишите свой хороший typeahead с отменой HTTP запросов на промисах и с rxjs, почувствуйте разницу.

Вы лучше сами попробуйте на практичной абстракции и почувствуйте разницу:


@ $mol_mem
typed( next? : string ) { return next || '' }

@ $mol_mem
suggestions() : string[] {
    const uri = `/suggestions?prefix=${ this.typed() }`
    return $mol_fetch.json( uri ).suggestions
}
В перемере, который я выслал, надо было оставить комментарии, конечно. Он был для внутреннего пользования.
Кода на rxjs получилось 6 строчек: с 55 линии по 61. Все что выше, это описание naive подхода, чтобы можно было показать ход мыслей от простого подхода к продвинотому.

Вы получили документ по ссылке example.org/foo/bar. По какой ссылке нужно его сохранять, чтобы не нарваться на двусторонний канал?

Не понял, что вы имели в виду. Ресурс по этой ссылке изменяется через PUT/PATCH на example.org/foo/bar. При чем тут вообще каналы?
Про спагетти я неверно выразился. Имелось в виду неявное состояние, которое является бичем любой системы и async/await этому активно способствует.
Зачем вы сравниваете ваниллу яваскрипт с rxjs?
С тем же lodash можно вполне уложиться в 3 строчки: stackblitz.com/edit/rxjs-pm9jc8?file=index.ts

Хотя пример и неплох в качестве джуниор-фильтра, к статье он подходит плохо в качестве иллюстрации.
Ваш пример не работает, если я сделаю 4 клика, то он сработает как тройной. И у вас опять же присутсвует неявное состояние count.
Не учел таких деталей, да собственно и не вдумывался особо.
Ну тогда проверку перенести в `_.debounce` функцию чтобы по окончанию она смотрела сколько кликов было. Тогда можно будет посчитать и сравнить.
Переменная `count` инициализирована выше. Заверните в closure если вы не хотите ее выдавать наружу, не вижу проблемы.

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

Таким же образом решаются задачи с typeahead — посмотрите их реализацию в куче других библиотек. Да, rxjs тут более оправдан (чем подсчет кликов по кнопке). Но опять же, в большинстве случаев просто берется готовая библиотека. Если у вас есть сильное желание написать «свое» — пожалуйста. rxjs или любой другой подход будут работать ± одинаково.
В знакомство с реактивным программированием, на мой взгляд, нужно добавлять сравнение Pull и Push моделей получения данных.
Спасибо, с удовольствием прочёл)
1. Есть какие-то best practices по скрещиванию RxJS-подхода с async/await-подходом? Например, «у меня есть observable, а нужен async generator» или «у меня есть async generator, нужен observable, а потом обратно async generator» — и все на TypeScript. Вот на эту тему статья не помешала бы.
2. Каков глубинный смысл в том, что http.get — observable, если он возвращает ровно одно значение? Чем здесь концептуально промис плох, который можно await?

Вопросы выше скорее в контексте серверной разработки, чем клиентской, кстати.

2) Просто для унификации интерфейса, чтобы без лишнего кода можно было сделать что-нибудь вида


race(http.get(proxy1), http.get(proxy2)).map(...convert response...).subscribe(...)

race — передаст в map первый пришедший ответ (от быстрейшего прокси).

Каков глубинный смысл в том, что http.get — observable, если он возвращает ровно одно значение?

Смыслов тут несколько. Во-первых, с fetch api можно получать и прогресс запроса, т.е. ваше утверждение не совсем верно. Во-вторых, и на мой взгляд самое главное, Promise нельзя отменить, а от Observable можно отписаться, что ведет к отмене запроса.
В RxJs есть оператор fromPromise для превращения Promise в Observable. Так же у observable есть метод toPromise делает обратно из observable promise.
Когда начинал во второй Angular, подумал нафиг observable, в каждом методе вызывал .toPromise() в конце. Потом понял что ошибся, теперь возвращаю Observable, при необходимости вешаю операторы или скрещиваю несколько Observable и далее
await myFinalObservable.toPromise().
Тогда и callbackHell не возникает и всю мощь и лаконичность RxJs можно использовать
Тоже начинал с toPromise))) думал зачем это все. Теперь честно делаю subscribe, но не из-за функционала больше, а потому что уж лучше что-то одно выбрать, везде использовать и не грузить зря мозг. Просто человеческий фактор.
Ага, интересно, давайте поговорим про toPromise(). Например, у меня есть observable, который выдает 1, 2, 3 и потом заканчивается. Если я делаю:

await observable.subscribe(val => process(val)).toPromise();
// кажется, не так — но как, кстати?

то тогда process() вызовется по разу для 1, 2, 3, а вызывающий поток управления будет await-ать окончания этого дела и потом продолжится. Великолепно.

Внимание — вопрос. Что, если process() — сама по себе async-функция, да еще довольно запутанная, и внутри нее может произойти исключение? Что в этом случае произойдет?

Ведь unhandled exception же скорее всего, нет?

Как через observable «замкнуть» все эти исключения на основном потоке управления, чтобы он не расползся, и чтобы гарантированно все исключения в зависимом коде не вышли за скоуп этого основного потока управления? Вот в чем вопрос.

(Под «скоупом основного потока управления» я понимаю такое свойство асинхронного куска кода, что его можно обернуть в try-catch, и из него ничего не «вылетит» неожиданного. Ведь это одно из самых полезных свойств async-await кода.)
// кажется, не так — но как, кстати?

await observable.pipe(map(val => process(val))).toPromise()

то тогда process() вызовется по разу для 1, 2, 3, а вызывающий поток управления будет await-ать окончания этого дела и потом продолжится.

Нет, промис зарезолвится последним значением — 3. И то, если стрим будет закрыт. Пока стрим не закроется промис не зарезолвится.


Что в этом случае произойдет?

Стрим закроектся, а промис зареджектится. Закрытие стримов при ошибках и невозможность их переоткрытия — та ещё боль. Приходится создавать дерево стримов заново.


Как через observable «замкнуть» все эти исключения на основном потоке управления

Да так же как с промисами — оператор catch. Логически промисы — частный случай обсерваблов.

Стрим закроектся, а промис зареджектится.

Точно? Напоминаю, process — async-функция. Если в map() передается коллбэк, возвращающий промис, то разве observable будет дожидаться резолва этого промиса перед тем, как выплевывать следующее значение?


В идеале что бы я хотел сделать, это сконвертить observable не в промис, а в async generator. Чтобы можно было написать типа такого:


try {
  for await (const v of observable) {
    await process(v);
  }
} catch (e) { ... }

В асинхронном случае надо вот так делать:


await observable.concatMap(val => from(process(val))).toPromise();

Также можно использовать mergeMap если порядок обработки не важен, а параллельность — напротив, как раз важна.

Вот же ж адище. Но спасибо, логика понятна!

И последнее еще — предложенный вами вариант с mergeMap (т.е. параллельно), но так, чтобы (три разных независимых варианта):
А) одновременно выполнялось не более N функций process() а остальные ждали своей очереди
Б) все разбивалось бы на чанки по N элементов, для каждого чанка process() выполнялись бы параллельно, а сами чанки — последовательно;
В) все развивалось бы на чанки, но не фиксированного размера, а до тех пор, пока не выполнится некоторое условие насчет элементов текущего чанка (такой write barrier как бы).

Как?

Пункт А — это штатная функциональность mergeMap.


Пункты Б и В — это уже вам нужен buffer и его вариации:


await observable.pipe(
    bufferCount(10),
    concatMap(buf => from(Promise.all(buf.map(process)))),
    concatAll()
).toPromise()

Для единообразия есть IxJS от той же команды, что и RxJS. Но толку мало, т.к. попытки совместить оба подхода, например в реплизации backpreasure, превращаются в научную проблему. Все лучше там, где дуплексное взаимодействие заложено изначально. Например streams из nodejs или callbaGs.

Странно, почему здесь до сих пор нет комментариев товарища vintage

Жду особого приглашения. Впрочем, а что тут можно сказать? Могу разве что предложить заинтересовавшихся темой, почитать эти две статьи:
https://habr.com/ru/post/307288/ — про асинхронное
https://habr.com/ru/post/317360/ — про реактивное

Еще пара ссылок к полезной информации:


  • пример самописного Observable — видео
  • раздел по RxJS в скринкасте по Angular на learn.javascript.ru
Вообще конечно не очень правильно говорить про реактивное программирование, и тут же уходить на RxJS. RxJS реактивен, но RxJS не единственен, и абстракции RxJS вовсе не о «простой» реактивности а-ля реакция на изменения, а о куда более конкретной реактивности (потоки событий или же push-коллекции). Архитектура RxJS чрезмерно конкретизирована — это реактивность в специфической обертке.

PS: А второй большой камень в огород RxJS — это вот эти самые магические методы на тему «как сделать с потоком то, что мне нужно». Без документации это абсолютно неподъемно, надо, наверное, писать конкретно на RxJS 24/7, чтоб помнить хотя бы треть методов. Я отлично помню времена, когда команде RxJS вздумалось переделать документацию, и какое-то время всё просто лежало, а потом еще какое-то время не было адекватного поиска. И разработка на эти дни просто встала.
О да, перевели внезапно на пайпы, а документация настолько убогая была, что приходилось в исходник лезть.
Я бы хотел рассказать предысторию появления этого доклада. У меня есть группа, которую я активно веду. Цель у меня прийти от базовых вещей к разработке spa на ангуляре. Поэтому я рассказываю про реактивность и делаю уклон в сторону rxjs т.к. ангуляр его активно использует. Поэтому на стриме и здесь я рассказал о том, что мы будем в дальнейшем изучать.
Еще один момент, по поводу того, что промисы нельзя отменять/прерывать, а observable можно.

Тут правильнее было бы, наверное, сравнивать observable не с промисами, а с async generator-ами. И вот их «отменять», кажется, можно — самой семантикой языка (промисы-то что отменять, они либо случились, либо еще нет):

async function something() {
  ...
}

async function* getStream() {
  for (let i = 0; i < 10; i++) {
    const v = await something(i); // или еще что-то, не важно
    yield v;
  }
}

for await (const v of getStream()) {
  if (v == “some”) break;
  ...
}


Так вот, если цикл выскочит на break, работа асинк-генератора getStream() тоже закончится. И потом сборщик мусора прибьет оставшиеся от него кишки. Отмена? Отмена.

Я, правда, не до конца понимаю, почему это работает. Наверное, потому что после break-а “for await” перестает «впрыгивать» обратно в генератор, поэтому он подвисает в воздухе (раз в него никто не впрыгивает), а потом сам генератор выходит из области видимости, и сборщик мусора вычищает все зависимые вещи прямо в середине того for-а внутри getStream().

Этот способ позволяет прерывать генератор только в yield-позициях, а await-позиции куда важнее.

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

Вот тут посмотри накидал на коленке банальных примеров stackblitz.com/edit/rxjs-hnlgh2
не надо ничего отменять

Ну да, и DDOS-атака на свой же бакенд своим же штатным фронтэндом — это совершенно нормально, не надо тут ничего менять!

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

Не "один лишний запрос", а "сотню лишних запросов с каждой открытой страницы".


Так-то понятно, что на один лишний запрос внимания можно не обращать...

Для таких кейсов тоже можно достаточно легко обойтись обвязкой для запросов к АПИ и вспомогательногьного класса который будет проверять актуальны ли ещё запросы или нет, и если нет, то просто их не выполнять и выбросить специальное исключение, а те что в pending отменить. И никаких RxJs не надо тут даже близко. Просто пишешь как обычно, понятный линейный код сверху вниз и при этом эти нюансы будут учтены уже в этой обвязке и хэлпер классе

То, что rx.js не обязательно нужна — это да, но начинали-то вы с того, что отмена запросов не нужна.

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

декларативный код (к которому относится RxJS) на порядок легче поддерживать, чем императивный.
Я надеюсь вы же шутите да?

Так себе декларативный. В сложных случаях нужно постоянно держать в уме какой оператор сколько подписок и в какие моменты держит...

В сложных случаях

А как себя поведет императивный подход в этих же случаях? Точно не лучше — «мешанина» будет на порядок сложнее.

Вот в rx.js она и оказывается сложнее...

декларативный код (к которому относится RxJS)

Не относится он ни к декларативному, ни к функциональному. Это скорее железнодорожное программирование.


на порядок легче поддерживать

Ну конечно. Любая не описанная в документации задача превращается в railroad tycoon. А описанная — в копипасту без глубокого понимания, что происходит.

Sign up to leave a comment.

Articles