Комментарии 50
На мой взгляд лучше взять простейшие промисы, async/await и допилить где надо, чем сразу брать Rx, потому что он как бы лишён недостатков промисов. Не надо забывать что главная проблема разработки — это сложность ПО, а ее недостаток фич. Ну я про свой CRUD все конечно, может у кого проблемы поглобальнее.
Согласен, что для простых проектов, когда у вас обычный запрос/ответ Rx избыточен. Да и технологии выбираются из расчета задач и компетенции команды.
А в целом RxJs хорош. Подписку не сделал — поток не работает, память не кушает; для работы с данными можно использовать единый интерфейс будь то клиентское событие или web-socket; связь один-к-многим вообще огонь, когда ты создаешь свой горячий поток.
Почему observable?
Потому что выбор правильных абстракций — это половина успеха, а весь наш мир — это один большой поток событий, который движется только вперед (кстати, поэтому двусторонний канал уже излишество) и именно поэтому CQRS шагает по планете вместе с функциональным программированием.
Чтобы прочувствовать rxjs попробуйте написать свой triple/quad click в качестве упражнения классически на таймаутах, а потом с rxjs. Я даю такое задание своим джуниорам. Тут можете подсмотреть ответ. У меня получилось уложиться в 5 строчек.
А чтобы его полюбить, напишите свой хороший typeahead с отменой HTTP запросов на промисах и с rxjs, почувствуйте разницу.
Что касается retry policy ну такое себе. Даже если оно и есть, то уж наверно в одном месте завернуто и по большому счету какая разница как оно реализовано.
Мне кажется вы упорно пытаетесь игнорировать мой аргумент, что всему свое место. Допустим даже все ваши примеры будут в реальном приложении. Это ещё совсем не говорит о том, что теперь нужен 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 этому активно способствует.
С тем же lodash можно вполне уложиться в 3 строчки: stackblitz.com/edit/rxjs-pm9jc8?file=index.ts
Хотя пример и неплох в качестве джуниор-фильтра, к статье он подходит плохо в качестве иллюстрации.
Ну тогда проверку перенести в `_.debounce` функцию чтобы по окончанию она смотрела сколько кликов было. Тогда можно будет посчитать и сравнить.
Переменная `count` инициализирована выше. Заверните в closure если вы не хотите ее выдавать наружу, не вижу проблемы.
Опять же, это просто иллюстрация примера натягивания совы на глобус. В данном примере rxjs ну не нужен от слова совсем. Да, его можно притянуть если вся остальная разработка на rxjs. В иных случая ни потоки, ни промисы для решения этой задачи не нужны.
Таким же образом решаются задачи с typeahead — посмотрите их реализацию в куче других библиотек. Да, rxjs тут более оправдан (чем подсчет кликов по кнопке). Но опять же, в большинстве случаев просто берется готовая библиотека. Если у вас есть сильное желание написать «свое» — пожалуйста. rxjs или любой другой подход будут работать ± одинаково.
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 можно отписаться, что ведет к отмене запроса.
Когда начинал во второй Angular, подумал нафиг observable, в каждом методе вызывал .toPromise() в конце. Потом понял что ошибся, теперь возвращаю Observable, при необходимости вешаю операторы или скрещиваю несколько Observable и далее
await myFinalObservable.toPromise().
Тогда и callbackHell не возникает и всю мощь и лаконичность RxJs можно использовать
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 как бы).
Как?
Для единообразия есть IxJS от той же команды, что и RxJS. Но толку мало, т.к. попытки совместить оба подхода, например в реплизации backpreasure, превращаются в научную проблему. Все лучше там, где дуплексное взаимодействие заложено изначально. Например streams из nodejs или callbaGs.
Жду особого приглашения. Впрочем, а что тут можно сказать? Могу разве что предложить заинтересовавшихся темой, почитать эти две статьи:
https://habr.com/ru/post/307288/ — про асинхронное
https://habr.com/ru/post/317360/ — про реактивное
Очень понятно и доступно
Еще пара ссылок к полезной информации:
- пример самописного Observable — видео
- раздел по RxJS в скринкасте по Angular на learn.javascript.ru
PS: А второй большой камень в огород RxJS — это вот эти самые магические методы на тему «как сделать с потоком то, что мне нужно». Без документации это абсолютно неподъемно, надо, наверное, писать конкретно на RxJS 24/7, чтоб помнить хотя бы треть методов. Я отлично помню времена, когда команде RxJS вздумалось переделать документацию, и какое-то время всё просто лежало, а потом еще какое-то время не было адекватного поиска. И разработка на эти дни просто встала.
Тут правильнее было бы, наверное, сравнивать 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-атака на свой же бакенд своим же штатным фронтэндом — это совершенно нормально, не надо тут ничего менять!
Не "один лишний запрос", а "сотню лишних запросов с каждой открытой страницы".
Так-то понятно, что на один лишний запрос внимания можно не обращать...
То, что rx.js не обязательно нужна — это да, но начинали-то вы с того, что отмена запросов не нужна.
понятный линейный код
декларативный код (к которому относится RxJS) на порядок легче поддерживать, чем императивный.
Так себе декларативный. В сложных случаях нужно постоянно держать в уме какой оператор сколько подписок и в какие моменты держит...
декларативный код (к которому относится RxJS)
Не относится он ни к декларативному, ни к функциональному. Это скорее железнодорожное программирование.
на порядок легче поддерживать
Ну конечно. Любая не описанная в документации задача превращается в railroad tycoon. А описанная — в копипасту без глубокого понимания, что происходит.
Асинхронное программирование в JavaScript (Callback, Promise, RxJs )