Pull to refresh

Comments 55

В RxJS на время асинхронного запроса состояние становится несогласованным.

Его не настолько сложно сделать согласованным чтобы считать это проблемой Rx:


concat(
  of({ loading: true }),
  request(...).pipe(
    map(data => ({loading: false, data})),
    catch(error => ({loading: false, error}))
  )
)

А что вообще такое "поведение по умолчанию" в случае Rx? Rx — это набор примитивов, которые гораздо мельче обсуждаемых вами, у них бесполезно искать поведение по умолчанию в рамках задач ОРП.

Поведение по умолчанию - это поведение, когда разработчик не написал дополнительного технического кода, чтобы изменить это поведение.

Акции... Хотяб экшены) или действия, ну никак не акции)

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

Дима, это же камминг аут!)хм. Код оказывается тоже имеет свойство эмпатии.

Если reaction - реакция, то action - акция. Логично же.

да, но в рамках своих ассоциативных связей.
Можно так же по логике перевести как реактьён и актьён )) А то и вообще не переводить. Это огромная нагрузка на восприятие — принятие словаря автора и поиска с ним уже существующей у себя терминологии.
action — действие, reaction — реакция, то есть действие, затронутое от какого то другого триггера. Да, на английском это круто сокращается и родственно звучит, но не переводится так оно гладко на русский. Я бы в скобках указывал привычную терминологию.

Эта шутка зашла уже слишком далеко.

Дима, за акции сейчас присесть можно, но не на фреймворк))

Реактивное программирование это довольно обширная тема. Тут не только стейт менеджмент, но и очереди, микросервисы и ещё куча всего. Могу порекомендовать книжку, где системно описаны разные юзкейсы: Reactive Design Patterns. Базовые термины и антологии есть в https://github.com/kriskowal/gtor . В вашей статье мне запомнились иллюстрации. Но текст, в местах где идёт отсылка к предметной области, выглядит немного дилетантским - как будто термины добавлены просто для красноречия.

Стейт менеджмент на фронтенде это довольно специфическая задача. Почему по-вашему ее решение нужно сводить к какому-то одному виду программирования, отбрасывая по пути все другие? Выглядит как якорение. Видимо с RxJS у вас какие-то личные счёты, но вы, например, смотрели IxJS, где как раз используется модель pull вместо push?

Прочитайте раздел "Ещё по теме" чуть более внимательно.

IxJS про однократное вычисление маленькими порциями. К реактивности оно имеет ещё более далёкое отношение, чем RxJS.

Можно подробнее о ваших претензиях к JSX и хукам?
Насколько я понял, вам не совсем нравится функциональный формат описания компонентов, правильно ли я понял?
Какие методики шаблонизации вместо JSX и обработки состояний вместо хуков вы считаете приемлемыми?

Тут про JSX. А про состояния, собственно, вся статья. ОРП отлично работает.

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

Не припомню такого за транзистором. Может мультиплексор?

Спасибо за статью, довольно структурировано.
Хочется чтобы автор пообщался с создателями реатома, может получится что-то интересное!

Он, к сожалению, не смог прилететь на эту дискуссию. Зато однажды я залетел на эту:

Попробую побыть адвокатом RxJS (хотя тоже его не люблю за излишнюю сложность)

  1. Style. Если сравнивать два приведенных примера, то объектный вариант выглядит, конечно, проще. Но вот насчет "сказать, что конкретно происходит" - RxJS однозначно выигрывает, там все конкретно написано, что происходит. Если исходить из того, что объектный вариант делает то же самое, то как там изменить debounceTime(0) на `throttleTime(40, animationFrame, {trailing: true})` - не понятно. Насчет потребления памяти - обычно все-таки гигабайты не на это тратятся, а на видео, канвасы, анимации: похоже на преждевременную оптимизацию.

  2. Dupes. Ну вот в RxJS достаточно легко выбрать, как сравнивать, по значению, структуре или через метод equals, например. И в каждом конкретном случае выбрать наиболее подходящий вариант. Даты из moment.js, например, не оч хорошо сравнивать структурно.

  3. Origin. Все равно нужен некоторый код в disconnectedCallback для отписки/удаления из slave, разве нет? И Pull реализуется на RxJS через shareReplay(1)

  4. Tonus. Defer реализуется через debounceTime(0), Lazy через shareReplay(1)

  5. Order. Пример с PostPage/Forbidden решается созданием верхнеуровнего компонента/роутера/guard. Если страница запрещена, то компонент демонтируется и отписывается. Ну и по-честному, Title и Body зависят от результата функции Guard. Если мы смогли получить данные без авторизации, то проблема на сервере.

  6. Flow. Кажется, что проблема свойственна OnPull. Сложное решение, похожее на магию - это лучше, чем ничего, но лучше бы не было и проблемы. Пример с забыли подписаться/отписаться сильно выдуманный: greeting$ = byTitle$.pipe( switchMap( byTitle => byTitle ? title$ : name$ )). Тут нечего забывать и нет магии.

  7. Error. Есть оператор catchError. Можно реализовать и Store, если функция подсчета длины слова будет возвращать number | Error.

  8. Extern. text$ = source_element$.pipe( async source => { await capture, await recognize...}). Точно так же возвращается промис и система рендеринга с ним справляется или нет. Если async|await не нравится, то можно source => new Promise.

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

RxJS однозначно выигрывает, там все конкретно написано, что происходит

Только если ты помнишь, что делают все эти операторы. А их сотни в RxJS. И то надо потратить время на их мысленную интерпретацию и не перепутать.

Если исходить из того, что объектный вариант делает то же самое, то как там изменить debounceTime(0) на throttleTime(40, animationFrame, {trailing: true}) - не понятно.

Точно так же, вставляете в нужном вам месте: throttleTime(40, animationFrame)

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

В реальном коде выбор обычно либо не делают вообще (и тогда работает поведение по умолчанию), либо делают неправильный выбор, что ещё хуже.

Даты из moment.js, например, не оч хорошо сравнивать структурно.

moment.js вообще не очень хорошо использовать. Есть куда более адекватные библиотеки. Впрочем, алгоритм глубокого сравнения может быть настраиваемым.

Все равно нужен некоторый код в disconnectedCallback для отписки/удаления из slave, разве нет?

С затягиванием обычно это всё автоматизировано.

И Pull реализуется на RxJS через shareReplay(1)

Это всё же не Pull, а шаринг стрима. Значения безусловно проталкиваются по стриму, пока на него есть подписки. При Pull, значения затягиваются при обращении к ним.

Пример с PostPage/Forbidden решается созданием верхнеуровнего компонента/роутера/guard.

Allow на диаграмме - это и есть тот самый guard.

Ну и по-честному, Title и Body зависят от результата функции Guard.

Это не поможет. Page должен быть вычислен до Title, а не только Allow. Да и что будет результатом вычисления Title при Allow= true ? Либо мусор, либо ошибка. Данных нет, с сервером всё в порядке.

Пример с забыли подписаться/отписаться сильно выдуманный

Я видел очень много кода с ручным subscribe. И часто с забытым unsubscribe. "Фу, дилетанты!" скажите вы? Да, люди не совершенны.

Сложное решение, похожее на магию - это лучше, чем ничего

В автотрекинге зависимостей и обновлениях в корректном порядке нет ничего сложного. Понять их даже проще, чем RxJS.

greeting$ = byTitle$.pipe( switchMap( byTitle => byTitle ? title$ : name$ )). Тут нечего забывать и нет магии.

Мне особенно нравится, когда после утверждения об отсутствии магии показывают такие вот заклинания, чтобы хоть как-то объяснить, как оно работает:

Интернет просто переполнен вопросами в духе "Чем switchMap оличается от flatMap?". Люди читают документацию и не могут понять.

Есть оператор catchError

Весь стрим до catchError будет прибит. А после него переключится на стрим-фоллбэк и уже не вернётся к работоспособному состоянию.

Можно реализовать и Store, если функция подсчета длины слова будет возвращать number | Error.

Предлагаете все колбэки заворачивать в try-catch, чтобы исключения не долетали до RxJS, а конвертировались в события, а также в каждом колбэке проверять а не пришло ли нам исключение место данных?

Точно так же возвращается промис и система рендеринга с ним справляется или нет.

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

Прелесть RxJS на мой взгляд, что на нем можно сделать все что угодно примерно одинаково сложно.

Предложите своё простое решение?

Только если ты помнишь, что делают все эти операторы. А их сотни в RxJS. И то надо потратить время на их мысленную интерпретацию и не перепутать.

А в языке JavaScript 1000 страниц спецификации, но это не мешает писать на нём.


Сотни стандартных операторов — это достоинство Rx, а не его недостаток.


Точно так же, вставляете в нужном вам месте: throttleTime(40, animationFrame)

А как этот самый throttleTime(40, animationFrame) вы реализуете, если его в коробке нет? А никак, ему нужно хранить своё состояние в вызвавшем атоме, что доступно только для операторов из коробки.


Интернет просто переполнен вопросами в духе "Чем switchMap оличается от flatMap?". Люди читают документацию и не могут понять.

Ну, такие люди. Картинка-то понятная, между прочим, особенно если взять картинку для flatMap и сравнить.


Весь стрим до catchError будет прибит. А после него переключится на стрим-фоллбэк и уже не вернётся к работоспособному состоянию.

А в исходное состояние его вернёт оператор switchMap или flatMap уровнем выше.


Ну вот система рендеринга в Ангуляр [...]

Это проблема системы рендеринга в Ангуляре, а не Rx.

А в языке JavaScript 1000 страниц спецификации, но это не мешает писать на нём.

Поэтому давайте удвоим это число, чтобы жизнь раем не казалась?

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

Тут я рассказывал, как работают файберы. А тут можете увидеть пример реализации дебонса на файберах.

Это проблема системы рендеринга в Ангуляре, а не Rx.

Она такая тупая не просто так, а потому, что в Rx не получить информацию о том, что где-то в стриме асинхронная операция идёт.

если ты помнишь, что делают все эти операторы, а их сотни

Из них штук 20 реально нужных и куча ерунды. Из этих 20 - 10 простых вроде map и filter. Но я соглашусь, что у RxJS слишком высокий порог входа. А у $mol разве нет?

Точно так же, вставляете в нужном вам месте: throttleTime(40, animationFrame)

Звучит круто) А можете кинуть документацию? А то не оч понимаю, про какую это билиотеку.

Это не поможет. Page должен быть вычислен до Title, а не только Allow. Да и что будет результатом вычисления Title при Allowtrue ? Либо мусор, либо ошибка. Данных нет, с сервером всё в порядке.

Тут непонятно. Page послеTitle, он ведь его содержит. При Allow=true в Title должен придти Post.Title, почему мусор/ошибка? При false ничего не должно придти.

Магия

В статье написано, что есть лагерь программистов, которые боятся магии автотрекинга зависимостей, только поэтому использовал это слово. Ну и уж точно автотрекинг сложнее понять, чем switchMap, в реализации которого 30 строк. Marble-диаграммы плохо помогают в понимании, да.

Ну вот система рендеринга в Ангуляр 

Плохая, но это не относится к RxJS.

Предложите своё простое решение?

Я бы это решал в бизнес логике на обычном JS - пришло обновление "важного" свойства - фильтруем, отдаем в ui. Если прям надо на RxJS, то из фильтра получаем компаратор игрушки и используем его в distinctUntilChanged перед фильтрацией.

upd: промахнулся веткой

Из них штук 20 реально нужных и куча ерунды.

Каждый выбирает себе 20 операторов, которыми решает все задачи, объявляя все остальные ерундой. Потом читает чужой код и не понимает его, ибо там человек выбрал другой набор нужных операторов.

А у $mol разве нет?

Нет, в $mol число абстракций меньше, чем операторов в Rx.

При false ничего не должно придти.

Вот именно, что не должно. Поэтому важно задавать в коде порядок обновления состояний, а не давать на откуп произвольно получившемуся динамическому графу.

Ну и уж точно автотрекинг сложнее понять, чем switchMap, в реализации которого 30 строк.

Возможно эта статья вам поможет с пониманием. Там автотрекинг уместился в те же 30 строк.

Я бы это решал в бизнес логике на обычном JS - пришло обновление "важного" свойства - фильтруем, отдаем в ui.

Список "важных свойств" зависит от содержимого функции фильтрации. Функция фильтрации выбирается пользователем в форме. При автотрекинге эффективная реализация тривиальна.

другой набор нужных операторов.

Ну не настолько там все плохо, зачем набрасывать-то

в $mol число абстракций меньше, чем операторов в Rx

Так давайте число абстракций $mol сравнивать с числом абстракций RxJS, а не с числом операторов. Так можно и с числом букв в сорцах сравнить. Но если Вы считаете, что $mol проще RxJS и порог входа ниже, то круто. Но пока я не вошел, спорить не могу)

 Там автотрекинг уместился в те же 30 строк.

Спасибо, действительно стало понятнее и не так страшен черт, как его малюют. Но switchMap все равно проще, ну.

При автотрекинге эффективная реализация тривиальна.

и правда здорово выглядит. Так и быть, попробую cellX

Таки попробовал cellx, удобно когда нет асинхронности. Можно использовать глобальную переменную для отслеживания зависимостей. С асинхронным кодом такое не сработает, т.к. другой "поток" может вмешаться и поломать. А делать разные контексты для разных потоков JS не умеет, точнее умеет для исключений, чем и можно воспользоваться и сделать псевдофайберы. Только вот как при этом перестать писать хитровымученный код?

Написал фильтрацию-сортировку товаров на RxJS, https://stackblitz.com/edit/react-ts-bwezv4?file=store.ts Да, это некрасиво, но если фильтр/сортировка вдруг станут асинхронными ничего не сломается.

А вот как сделать драг-н-дроп в cellx или $mol я совсем не понимаю. Для конкретизации: юзер нажимает мышью на div, и может его перемещать. При отпускании мыши/выходе за пределы области перетаскивание заканчивается.

Прекрасно. Меня вот эта вот строчка особенно порадовала на каждое изменение любого продукта:

map((x) => new Map(x.map((p) => [p.Id, p]))),

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

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

А вот как сделать драг-н-дроп в cellx или $mol я совсем не понимаю.

Заводите реактивную переменную "я дрегендроплюсь" и если она true - перемещаете вслед за координатами мыши, которые тоже реактивные.

При выходе за пределы области перетаскивание заканчивается.

Не стоит так делать.

То, что меня не волнуют тормоза пользователя - это Ваши догадки и необоснованные обвинения. Если что, минусы не я Вам ставлю.

Что я буду делать, если требования существенно изменятся? Выбирать инструмент исходя из требований. В этой задаче RxJS не нужен, а cellx/$mol/mobx похоже, что отлично подходят. Правда меня смущают O(N) reactions и O(N*logN) добавлений зависимостей в Set. Но это ладно, я скорей всего ошибаюсь с оценками и это не оч страшно. Я повторюсь, что решал бы на уровне бизнес-логики, используя любую библиотеку, которая подойдет. Но не строил бы все приложение на этой библиотеке.

перемещаете вслед за координатами мыши

Вот тут есть проблема. Нужен асинхронный requestAnimationFrame, верно? А еще суммировать сдвиги mousemove между соседними фреймами или считать разницу координат мыши - в любом случае нужно несколько значений координат мыши. В rxjs это решается через buffer, window и подобные. Я представляю, что это получится сделать через псевдофайберы, но код должно быть будет пахнуть.

За setPointerCapture спасибо, не знал.

А еще суммировать сдвиги mousemove между соседними фреймами

А нельзя брать изначальные (на момент начала dnd) координаты мыши и текущие? Т.е. игнорировать "соседние фреймы" от слова совсем. Или в этом случае не нравится поведение когда курсор выходит за границы экрана и возвращается?

В некоторых задачах может сработать, хорошая идея.

В правильной реализации зависимость добавляется за О(1).

Вы предлагаете при каждом незначительном изменении требований менять библиотеку и переписывать весь код?

Не понял, при чём тут rAF. Но, как ниже заметили, достаточно запомнить координаты на начало перемещения. Вообще, dnd - это довольно лайтовая задача. Есть куда более интересная: синхронизация состояния в памяти с indexedDB, с другими пирами по webrtc и с сервером по веб сокетам. При этом соединения надо автоматически восстанавливать при их подвисании, и при этом не потерять данные.

Вы предлагаете при каждом незначительном изменении требований менять библиотеку и переписывать весь код?

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

при чем тут rAF

При том, что мне непонятно, как это сделать, ну и он определенно нужен для плавного перемещения. Возможно я не очень правильную задачу придумал, но идея была вот в чем: как использовать cellx/$mol, когда нужно несколько последних состояний или в середине цепочки должен быть асинхронный инвариант, от которого зависят дальнейшие подписки.

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

Если же вам всё же нужно несколько последних значений (например, последовательность нажатий для суперудара в файтинге), то просто заводите массив для n последних значений и аппендите к нему очередное значение.

Видимо у нас с Вами разная специфика задач - в моих есть сложность в обработке нескольких последовательных действий пользователя с учетом их временных характеристик: долгий клик отличается от короткого клика и от клика с перемещением мыши. И я хотел бы уйти от RxJS в этом кейсе, но пока что не вижу, куда. А проблем с клонированием массивов или создания мапы из массива нет - не такие большие массивы. Ну и это не сравнимо с постоянной перерисовкой нескольких канвасов размером в 4 экрана.

Спасибо за беседу, почерпнул из нее много полезного.

А что за специфика с огромными холстами?

Карта вроде гугловской, с большим количеством метеоданных, которые периодически меняются. Разные слои на разные канвасы или svg. Много алгоритмов в воркере, оттуда и рендеринг черз offscreencanvas.

Так а огромные холсты зачем? Тут лучше рендрить лишь то, что попадает в видимую область. И на pull рактивности это делать проще всего. Я так редактор документов на сотни страниц делал с рендерингом на холсте.

Чтобы был запас по краям и юзер не смотрел на белые полосы. Ну и в случае ретины нужно удваивать еще.

Зачем на ретине удваивать? Не думаю, что на карте пользователи заметят разницу. Если, конечно, они делом занимаются, а не на скриншоты любуются.

Дизайнеры дотошные с макбуками и гендир. Ну и незначительно это утяжеляет внезапно

А еще там много мелкого текста и значков, которые при мыле сильно плохо читаются. Что вообще странно, там и без ретины хорошее разрешение.

Размытие скорее всего из-за непопадания в пиксели. Решается подбором соответствующего размера и позиции. В случае шрифтов - хинтами, которых у кастомых шрифтов обычно нет.

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

Я, когда в роуминге интернетом пользуюсь, очень радуюсь, что у меня не ретина.

Не думаю, что на карте пользователи заметят разницу.

Людям с хорошим зрением это сразу бросается в глаза. Говорю как человек которому прилетел тикет про "мыло" в рендере SVG на канве mapbox-а :) Оказалось, я не совсем правильно задействовал API.


Я, когда в роуминге интернетом пользуюсь, очень радуюсь, что у меня не ретина.

Хехе. Это точно. "Правильно" сделанный медиа-сайт для ретины грузит 4х картинки и видео. Хардкорный удар по трафику.

Дело не в зрении, а в том, на что у человека направлено внимание. Если он сам продуктом не пользуется, а только проверяет, то и не такие тикеты могут прилетать. Бизнес велью они продукту не добавляют.

Смотря что у вас за проект. У нас "business value" это как раз и есть картинки (проект вроде instagram-а).
P.S. минус не от меня

Ну вот у того же инстаграма плотность пикселей не влияет на бизнес велью. Иначе бы они так адово не жали jpeg. А вот что именно изображено - очень даже.

А вот как сделать драг-н-дроп в cellx или $mol я совсем не понимаю

ОРП реализации из статьи заточены в первую очередь на использование в стейт-менеджерах, для обработки пользовательского ввода нужно будет самому дописать некоторые вещи идущие в RxJS из коробки. С другой стороны RxJS не очень годится для использования в стейт-менеджерах, т.к. в основе стримы вместо атомов/ячеек и это создаёт дополнительные трудности, порой довольно неприятные.
Набросал простейшую реализацию dnd на cellx, она конечно не идеальна, но как пример вполне сгодится: https://riim.github.io/cellx/docs/examples/simple-dnd.html .

const Name = new BehaviorSubject( 'Jin' )

const Count = Name.pipe( map( Name => Name.length ), distinctUntilChanged(), debounceTime(0), share(), )

const Short = Count.pipe( map( Count => Count < 4 ) distinctUntilChanged(), debounceTime(0), share(), )

Что и зачем делает этот код на RxJS не сможет сходу сказать даже опытный стример. А это ведь самый простой пример, далёкий от реальной жести.

Расшифровка rxjs кода:

Создаем subject с дефолтным состоянием Jin;

Создаем Observable и кладем в константу Count;

В операторе map возвращаем длину строки Jin

Добавляем distinctUntilChanged для предотвращения дублирования если в потоке будет постоянно одно и тоже значение;

Вызываем задержку в 0 секунд непонятно зачем, и делаем multicasting для перевода Observable из холодного состояние в горячее;

Создаем Observable и кладем в константу Short;

В операторе map возвращаем boolean из выражения Count < 4;

Добавляем distinctUntilChanged для предотвращения дублирования если в потоке будет постоянно одно и тоже значение;

Вызываем задержку в 0 секунд непонятно зачем, и делаем multicasting для перевода Observable из холодного состояние в горячее;

Вызываем задержку в 0 секунд непонятно зачем

О чём и речь.

делаем multicasting для перевода Observable из холодного состояние в горячее;

Не для этого.

Sign up to leave a comment.