Комментарии 143
PS: А, ну и да. Статью подтверждаю своим опытом — сам тоже рефакторил лапшекод на коллбеках на MobX, и результатом был крайне доволен.
Насколько я помню там используется менее продвинутый алгоритм, который я описывал тут: https://habr.com/ru/post/235121/
Riim поправь, если я не прав.
Начиная с v1.8 алгоритм тот же, что и в mobx. А на счёт менее продвинутого я бы поспорил, да в старом алгоритме есть один мелкий нерешаемый недостаток, но зато он минимум в 3 раза быстрее. В три раза медленнее — это лучшее, что я смог получить с нового алгоритма избавившись всего лишь-то от одного недостатка с которым вполне нормально жилось. Довольно сомнительное улучшение.
Корректность всё же важнее производительности, да и производительность ты тогда измерял на не слишком реалистичных сценариях типа глубины в тысячи зависимостей. Или есть какие-то новые более адекватные бенчмарки?
Корректность всё же важнее производительности
С корректностью результатов в старом алгоритме всё отлично. Недостаток в лишнем вычислении при определённых условиях, которое в реальном приложении случалось крайне редко и совершенно не мешало.
Или есть какие-то новые более адекватные бенчмарки?
Не понимаю почему ты не веришь в адекватность такого бенчмарка. Ну вот уменьшу я глубину (число слоёв) одновременно увеличив количество ячеек в каждом слое и что дальше? Если количество вычисляемых ячеек по которым пройдёт сигнал будет тем же, то и результат будет абсолютно тем же.
Все бенчмарки для такой библиотеки, скорей всего, можно разделить на два типа: 1 — скорость создания экземпляра ячейки, 2 — скорость прохождения сигнала по вычисляемым ячейкам. Остальное практически не имеет какого-либо смысла. Что ты предлагаешь ещё замерить? Скорость чтения не вычисляемой ячейки? Зачем?
Оно не просто лишнее, а возникало когда состояние уже было изменено не совместимым с этим вычислением способом. В зависимости от топологии оно могло стрелять и редко, но когда стреляло — отрывало руки по локоть.
Потому что от топологии очень сильно зависит эффективность разных алгоритмов. И надобности в сильно глубоких деревьях мне встречать не приходилось.
Что можно замерять:
- Трекинг большого числа зависимостей (фильтр списка товаров).
- Инвалидация большого числа зависимых (подсветка текущего элемента в большом списке).
ЗЫ: Лично мне вот, кстати, от хорошей реактивной библиотеки за эти годы экспериментов и возни стало надо следующее:
а) Чтоб было про стейт, а не потоки событий. В гробу я видел потоки событий;
б) Чтоб можно было без шума и пыли динамически фигачить зависимости и менять/убирать их;
в) Чтоб не было тормозов, когда зависимости начинают исчисляться в сотнях;
г) чтоб можно было в свойствах делать Symbol (что MobX в это не умел — для меня в свое время стало очень неприятным сюрпризом, из-за которого качество кода заметно просело).
Зачем надо г)? Чтоб можно было на полученные от бека данные вешать реактивные поля, и потом не париться тем, что перед JSON.stringify обратно на сервер эти реактивные поля нужно будет вычищать.
Про глубину в очередной раз ответил чуть раньше.
Чтоб можно было на полученные от бека данные вешать реактивные поля, и потом не париться тем, что перед JSON.stringify обратно на сервер эти реактивные поля нужно будет вычищать.
я для подобного использую декоратор NonEnumerable.
Чтоб не было тормозов, когда зависимости начинают исчисляться в сотнях;
У меня на текущем проекте их до 3k, а зависимых так вообще до 500к :-)
Чтоб можно было на полученные от бека данные вешать реактивные поля, и потом не париться тем, что перед JSON.stringify обратно на сервер эти реактивные поля нужно будет вычищать.
Их можно просто объявлять неитерируемыми.
но когда стреляло — отрывало руки по локоть
можно пример? Я находил у себя в приложении пару мест с таким лишним вычислением, там всё гладко было. Я именно намеренно искал, никаких проблем не возникало.
И надобности в сильно глубоких деревьях мне встречать не приходилось
вычисляемой ячейке где-то в середине цепочки вообще без разницы сколько там в глубину над или под ней. Твоё предложение менять число зависимостей ещё хоть как-то обосновано (ответил ниже), но я хоть убей не понимаю, что ты так вцепился в глубину дерева. Это же просто способ увеличить число ячеек в тесте.
Трекинг большого числа зависимостей
Инвалидация большого числа зависимых
я вижу у тебя для хранения родительских и дочерних ячеек используется Set, has на котором, при достаточно большом количестве айтемов, будет заметно быстрей, чем indexOf на массиве (https://jsperf.com/array-indexof-vs-set-has/). Наверно, в таком кейсе твой вариант действительно будет быстрее. Я же выбрал массив, тк. при малом числе айтемов уже indexOf быстрее, плюс нативный for-of тогда ещё рано было использовать, а Set#forEach был заметно медленнее обычного цикла. Малое число зависимостей — это 99.9% случаев. Мой бенчмарк показывает как ведут себя библиотеки в этих 99.9%, ты же предлагаешь мне бенчмарк под 0.1% заявляя, что он будет более адекватным. Мне кажется ты всё же не прав.
можно пример?
Показ страницы зависит через несколько атомов от урла, а на странице идёт вычисление на основе параметра из урла с меньшей глубиной зависимостей. Страница предполагает, что раз она открыта, то параметр в урле есть. Но когда уходишь — параметр тоже стирается. В итоге вместо параметра приходит балалайка и в лучшем случае начинаются тормоза из-за кучи эксепшенов и ненужных вычислений, а в худшем — что-нибудь ломается капитально.
не понимаю, что ты так вцепился в глубину дерева. Это же просто способ увеличить число ячеек в тесте.
В реальном приложении дерево будет более широким, а с большим числом зависимостей разные либы справляются по разному.
я вижу у тебя для хранения родительских и дочерних ячеек используется Set, has на котором, при достаточно большом количестве айтемов, будет заметно быстрей, чем indexOf на массиве
В новой реализации уже используются массивы, но без indexOf.
а в худшем — что-нибудь ломается капитально
ok, на самом деле я немного о другом случае описанном здесь. А то о чём говоришь ты похоже из этой серии. Странно, но со мной такого ни разу так и не случилось, может мне очень повезло, а может что-то в моём фреймворке не даёт этому происходить. В любом случае хорошо, что ничего подобного больше не случится ни с $mol_atom, ни с cellx.
В реальном приложении дерево будет более широким, а с большим числом зависимостей разные либы справляются по разному.
так я и не спорю про разное число зависимостей, я про то, что замена большей глубины на большую ширину без изменения числа зависимостей ничего не даст. Так зачем ты это требуешь?
но без indexOf
выглядит довольно запутанно, плюс отталкивает slaves с разными типами значений (в masters как я понял тоже самое) и лишние итерации при переборе masters (if( !master ) continue
). Set#has конечно чуть медленнее, но неужели вся эта дополнительная возня действительно даёт результат? Можешь поверхностно объяснить идею?
я про то, что замена большей глубины на большую ширину без изменения числа зависимостей ничего не даст.
Разные реализации по разному справляются с разным числом зависимостей. Ну грубо говоря indexOf на большом числе зависимостей может начать тормозить.
Можешь поверхностно объяснить идею?
Идея в том, чтобы
- трекинг происходил как можно быстрее
- требовалось как можно меньше памяти
- можно было трекать по позиции, что необходимо для квантования вычислений
То есть в процессе работы некоторые подписки могут задублироваться, но это не страшно — пропустить их не долго.
Да, можно было бы разбить на два массива и даже возможно получилось бы немного быстрее и экономней по памяти, но дебажить такое сложно.
Разные реализации по разному справляются с разным числом зависимостей. Ну грубо говоря indexOf на большом числе зависимостей может начать тормозить.
так и я о том же. Глубину то ты зачем уменьшить хочешь? Ты не понимаешь, что увеличивать число зависимостей можно не трогая глубину?
трекинг происходил как можно быстрее
ну это понятно, я про идею алгоритма спрашивал.
Ну да, просто тестировать имеет смысл на реалистичных сценариях.
Ну там долго расписывать, какая именно часть его не понятна?
Ну да, просто тестировать имеет смысл на реалистичных сценариях.
ну там вполне реалистично, 1-2 зависимости, можно увеличить до 1-3, но не более, это будет похоже на 99% ячеек в реальном приложении. А про глубину нет смысла думать, как я уже говорил, ячейке пофиг на какой она глубине, на скорости её обработки никак не отражается, это просто способ сделать их много.
Ну там долго расписывать, какая именно часть его не понятна?
в общих чертах вроде понял, вопрос уже другой: это реально даёт результат в плане ускорения или это пока просто эксперимент? Чем больше понимаю алгоритм, тем большее ощущение, что это никак не должно быть быстрее использования Set и тем более массива при малом числе зависимостей.
codesandbox.io/s/goofy-hamilton-b9jxv
codesandbox.io/s/frosty-dust-2mk5b
Это лишь второй проект на этой библиотеке, наверняка чего-то не знаю ещё
Только это стремный и некрасивый способ, получать store через пропсы
А какие еще варианты в реакте есть? Речь не о глобальном стейте, конечно, а, допустим, такая задача — у нас есть компонент Х, в нем список элементов E, для E при этом уже есть свой стейт и своя локлаьная огика. С-но, стейт Х содержит список стейтов E, как передать каждый из стейтов E в нужный E без пропсов?
Не, так я говорю именно против inject который стор в пропсы пробрасывает, когда стор можно просто подключить через Import
Я же указал, что речь не о глобальном сторе, речь о локальных сторах — которые привязаны к инстансу компонента, внутри него создаются и с ним же уничтожаются. Т.е. импортировать вы можете конструктор, но не сам стор. Если надо оркестрировать такие сторы из внешнего компонента, то никак в реакте кроме прокидывания в пропсы это не сделать. Ну разве что какое-то жуткое обмазывание провайдерами и хоками.
Я хоть и не страдаю DI в любом месте проекта, но глупо отрицать, что DI в среднем даёт куда более пригодный к переиспользованию код. Скажем, в том, чем занимался я, я в некоторых read-only случаях вместо настоящих сторов передавал простые POJO такой же структуры, где мне реактивность заведомо не требовалась, и гонять зря весь движок MobX было не надо.
Ну и передать стор — это мягко говоря не props hell. Это вы настоящего props hell не видели.
Как по мне, так DI это вырви глаз и лишний код, все можно делать компактнее и красивее. Provider использую только когда нужен Context API.
Поэтому чтобы реактивную технологию скрестить с не реактивной, без этого ни как.
DI и реактивность — это вообще две ортогональные друг другу вещи.
React сам по себе вообще не реактивный, от слова совсем. Поэтому чтобы реактивную технологию скрестить с не реактивной, без этого ни как.
Ну вот тут как-то без костылей обошлись: https://github.com/eigenmethod/mol/tree/master/jsx/view
onclick
серьезно? не camelCase и не under_score, печально) onclick={ event => this.change( event ) }
вместо того чтобы передавать функцию по ссылке, давайте ее каждый раз по новой создавать, круто)серьезно? не camelCase и не under_score, печально)
Серьёзно: https://developer.mozilla.org/ru/docs/Web/API/GlobalEventHandlers/onclick
вместо того чтобы передавать функцию по ссылке, давайте ее каждый раз по новой создавать, круто)
Это не так дорого, как вам кажется.
а просто будете делать import вашего стора/сторов, то вообще будет сказка
не учите плохому, это фактически тот же самый
MyThing.getInstance()
Для прямого связывания сторов (моделей) есть DI через конструктор (poor man's DI ручками через конструктор),
для связывания UI компонент с ними есть DI или на пропсах, или сырой реактовый контекст, или нормальные IoC абстракции над контекстом (InversifyJS)
А ваш совет подходит для TODO-app на нескольких синглтонах.
Это троллинг?
Есть такое понятие умный и глупый компонент.
Эти понятия не масштабируются. И являются по сути костылями к Реакту, не позволяющему управлять состоянием компонент извне.
Для меня пока Provider и inject просто создают иллюзию, что между стором и остальными компонентами нет зависимости. А зависимость то никуда не делась.
Вот смотрите. У нашего SPA есть index.js, там написано import Store from /path1. Далее в компоненте мы просто inject(«storeName») и не думаем, откуда он пришёл сверху и какому файлу соответствует. Увели этот компонент на другой проект, в компоненте ничего не меняем вообще, просто в index.js нового проекта пишем import Store from /path2.
Эти наши изменения никак не затрагивают сам компонент. А вот если писать в компоненте import Store from ./path, то просто так Store мы не поменяем. Надо будет копировать компонент в другую папку, где по такому же относительному пути лежит другой Store. Или, ттт, писать бизнес-логику подключения в самом компоненте.
Поэтому через Provider выходит универсальнее
А используя inject тайпскрипт или флоу покажет ошибку если не был прокинут стор через Provider? Преимущество импортов в том что они статически тайпчекаются и ошибки будут видны сразу а вот все эти инжекты и провайдеры (и прочие di) под большим сомнением (причем если и тайпчекаются то скорее всего нужно будет заимпортить тип чтобы передать дженерик инжекту и тогда смысл в этом вообще пропадает если все равно связь через импорты)
Когда падает? В рантайме? В момент рендера нужного компонента? То есть для того чтобы задетектить ошибку непрокинутого стора нужно будет запустить приложение, наклацать нужное состояние чтобы отрендерился нужный компонент и только тогда в консоли увидеть ошибку отсутствующего стора? Нет уж, спасибо, я лучше буду импорты юзать с которыми тайпскрипт сразу в редакторе подсветит ошибку в момент копирования файла с компонентом в проект
И что у вас там такого, из-за чего нельзя просто через import передать нужный стор?
В 2k19 приходится объяснять что не так с глобальными переменными...
Передача через Provider или через import, делает стор локальным или глобальным?
При том, что то, что находится за import — глобальные переменные. Просто вынесенные в отдельный файл.
Проблема глобальных переменных не в этом. А в том, что они (внезапно) глобальные. То есть все пользователи этой переменной работают безусловно с одним и тем же состоянием, без возможности их развязать.
То, о чём вы говорите — это глобальный скоуп. Разумеется, всё, что лежит в глобальном скоупе является глобальным (то есть в единственном экземпляре в приложении). Но это далеко не единственное место. Если вас смущает термин "переменная", то можете изменить его на термин "стейт". Суть не изменится. Далее в этом терминологическом споре я не участвую.
Да, он становится свойством контекста окружения.
Ок, теперь вроде вижу пользу от такого вида внедрения зависимостей. Это для случаев, когда в рантайме в компоненте планируется использовать разные экземпляры классов, передаваемых в него.
Возвращаясь к ситуации со сторами, не так часто используется несколько экземпляров одного и того же стора. Я думаю, использование для сторов Provider и inject все-таки чаще будет избыточным.
Может еще есть что-нибудь полезное в таком виде DI?
Вот codesandbox.io/s/goofy-hamilton-b9jxv поиграйтесь, проверьте что все пашет.
Вот ещё codesandbox.io/s/frosty-dust-2mk5b тут прям показана сама соль эффективного рендера React + MobX
С inject я могу прибить стор к компоненту и отрендерить его. А могу просто прокинуть через пропсы значения из родительского
codesandbox.io/s/magical-golick-b1675
Provider и Consumer стоить использовать только в рамках Context API
Вопросов больше нет
В одном месте нужен один инстанс сьора, в другом — другой. Контекст эту проблему решает через разных провайдеров. Импорт — разные файлы нужно создавать, так?
Подключение стора через import сделает невозможным SSR.
Или можно построить нормальную архитектуру без костылей в виде жирного браузера, подставляющего переменные в текстовый шаблон.
2) Что именно вы называете костылем в виде жирного браузера?
3) Нормальная архитектура в вашем понимании это какая? Больше конкретики пожалуйста, какой стэк технологий и т.д.
Я сам предпочитаю обычный импорт из-за простоты, но в данном случае нужно признать, что это может вызвать проблемы. Лучше доставать стор из контекса реакта.
Костылем я назвал puppeter.
Разговор был не о готовой архитектуре, а о том, что мы строим под приложение. Но если хотите пример хорошей архитектуры, где уже решены эти вопросы, посмотрите фреймворк DerbyJS.
Пробежался по всем страницам и положил их в кэш. И бэкенд их отдает из кэша, отсюда максимальная производительность.
Когда меняется контент у какой-то страницы, просто рендеришь ее ещё раз и ложишь в кэш.
Отсюда вывод, никакое DI и прочее гуано использовать не нужно.
Нет смысла делать рендер на каждый запрос и отдавать максимально свежие данные ради SEO.
в индустрии в последнее время популярны фреймворки, реализующие Flux-архитектуру фронт-енд приложения
Это какие такие фреймворки кроме Реакта и его клонов? FLUX не масштабируется, так как предполагает один глобальный цикл обработки данных. Вот даже вам для этого пришлось делать две одинаковые иерархии: для сторов и для шаблонов.
этим декоратором должен обёртываться любой хэндлер
Я бы сказал, что не должен, а может. Если не обернуть в @ action, то при каждом изменении observable переменной внутри функции будет производится перерендеринг зависимых от этих переменных компонентов. С использованием этого декоратора перерендер будет произведён один раз в конце. То есть для функций меняющих одну переменную его можно и не использовать
В целом имея опыт написания проекта полностью на функциональных компонентах с хуками, могу сказать, что в следующий раз я хуки использовать не буду, слишком у них много недостатков, а их достоинства можно так же подтянуть в классы и делать не extend React.Component а extend MyEnchancedComponent в котором уже можно расширить функционал как удобно. Тот же useEffect в классах реализовать элементарно. И те же кастомные хуки тоже делать не проблема и использовать в классах. Вот пожалуйста codesandbox.io/s/pedantic-silence-xu0jd как раз на примере излюбленного онлайн статуса пользователя который в примерах реакта.
Скорее всего ваше решение сломается, так как Реакт трекает состояния хуков основываясь на порядке обращения к ним при рендеринге. А так как конструкторы компонент отрабатывают при рендеринге родительского компонента, то любая динамика в нём сломает ваши хуки капитально. Весёлой отладки.
codesandbox.io/s/epic-wave-0zt26 — ещё есть вопросы?
Предлагаю вам почитать этот доклад, чтобы понять как реакт трекает хуки: https://habr.com/ru/post/413791/
Он не про хуки, но принцип тот же.
Также можете поразмышлять над тем, почему Реакт запрещает использовать хуки в циклах и условиях, — причина всё та же.
Придумывать пример, на котором у вас всё рухнет, у меня нет ни времени, ни желания. Я вас предупредил. Далее уже вы сами решайте, делать ли ставку на опасное решение, или подождать, пока грабли ударят вам по лицу.
codesandbox.io/s/epic-wave-0zt26
P.S. Конечно у вас нету желания, что-либо доказать, потому что, вы как обычно не правы и сядите в лужу. Это и так понятно.
Ага, не обратил внимание, что вы не реактовые хуки используете, а свои.
А желания во многом нет из-за манеры вашего поведения. Подумайте над этим.
Лучше по каким параметрам и для кого? Только не говорите "по всем и для всех". Тут грамотного разработчика на реакт на зп чуть выше рынка сложно найти, а как найти на Mol?
Ищите просто грамотного разработчика, а он уже быстро освоит нужный инструмент, будь то Реакт с его 100500 библиотек-костылей, Ангуляр с тоннами бойлерплейта или Мол с необычным синтаксисом.
Мы предпочитаем исключать ситуацию когда разработчику не нравится стэк, когда он просто его терпит за "чуть выше рынка".
Вы хотите сказать, что всем "реакт разработчикам" нравится Реакт?
Я вам по секрету скажу: мобыксу реакт — как собаке пятая нога. Первое, что делает mobx-react — это выпиливает основу реакта — реконциляцию vdom.
Зато сам mobx становится менее классным, ибо шаблоны в реакте реализуют push семантику, а не pull.
Он делает его реактивным :)
Не всем, но мы искали не таких, что будут его терпеть за деньги
Они якобы должны делать лучше то, ради чего появились.
reactjs.org/docs/hooks-intro.html
Вкратце опишу свои мысли по этому.
1) It’s hard to reuse stateful logic between components
В доках написано «React doesn’t offer a way to “attach” reusable behavior to a component (for example, connecting it to a store).» Как бы это проблема реакта, а не классов. На классах такое сделать можно. Например, в соседнем комментарии MaZaAa предложил вариант решения. Но так как он не встроенный, как хуки, то и выглядит громоздко.
Миксины были близки к удобному и качественному решению проблемы. Но смешивание в таком случае — это ошибка. Данные и методы в миксине не стоило смешивать с компонентом, а надо было делать ее отдельный объектом, который имеет такой же жизненный цикл, как и компонент. Каждый такой объект имел бы свои данные, а не общие, мог бы взаимодействовать с другими такими же объектами и компонентом. Было бы еще очень полезно, если бы их через атрибуты можно было бы добавлять, тогда ради небольших фич не пришлось бы создавать компоненты, как сейчас приходится.
2) Complex components become hard to understand
То, что было в классе, поместили в функцию и поменяли API. Не знаю как другим, но для меня лучше не стало.
3) Classes confuse both people and machines
В таком виде данное утверждение очень спорное и даже ошибочное.
Не зря же в JS добавили классы, хотя их функционал можно на прототипах сделать.
Кому-то удобней классами делать, кому-то функциями. Нормально два варианта оставить, но вот пропагандировать только один, это глупо. Не тот случай.
На классах такое сделать можно. Например, в соседнем комментарии MaZaAa предложил вариант решения. Но так как он не встроенный, как хуки, то и выглядит громоздко.
А почему громоздко?
Использование в одну строчку
this.userState = useUserOnlineStatus(this, props.userId);
Код самого «хука» такой же по сути как и для функциональных компонентов
const useUserOnlineStatus = (context, userId) => {
const userState = new UserState();
context.useEffect(() => {
const handleStatusChange = status => {
userState.online = status;
};
onlineSubscriber.subscribe(userId, handleStatusChange);
return () => {
onlineSubscriber.unSubscribe(userId, handleStatusChange);
};
});
return userState;
};
Я имел ввиду, что в вашем решении надо еще BaseComponent реализовать. А в случае хуков уже встроенное стандартное решение. А так, я бы не сказал, что ваше решение уступает хукам.
Гляньте как во vue это всё порешали: https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md
В плане переиспользования кода преимуществ не увидел. Точнее, перед миксинами есть преимущество, т.к. поля миксины объединяются с полями компонента. Но это не столько заслуга хуков, сколько ошибка в архитектуре vue. Если вместо миксин сделали бы изолированные объекты, то получили бы такие же возможности переиспользования, как и с хуками.
То, что явно лучше на уровне компонента: не надо дублировать логику, которая нужна на каждый рендер, в didUpdate/didMount, чуть меньше кода при работе со стейтом, если в компоненте нет или мало "глобальных" изменений стейта, изменений нескольких ключей стейта одновременно, а вот строчки типа
setLoading(false)
setOptions(options)
setSelected(null)
setTooltipMessage("Select option")
вместо одного setState выглядят, скажем так на любителя.
Главный плюс на уровне проекта — простое переиспользование. Но с главным минусом (для меня) — не работают с классами — на большом проекте, где в основном классы, толку от этого мало, если не ставить целью переписать всё на функции с правилами типа "изменения файлов с классами компонентов пройдёт ревью только если компоненты переписываются на функции"
То, что явно лучше на уровне компонента: не надо дублировать логику, которая нужна на каждый рендер, в didUpdate/didMount, чуть меньше кода при работе со стейтом, если в компоненте нет или мало "глобальных" изменений стейта, изменений нескольких ключей стейта одновременно
Так это же все можно и для классов сделать. Например, никто не мешал добавить life-cycle метод, который будет работать как didUpdate+didMount.
Главный плюс на уровне проекта — простое переиспользование.
С-но в классы точно так же можно выносить куски работы со стейтом. Единственное что делают хуки — позволяют получить автоматом нужный инстанс сервиса. Но, во-первых, это снижает контроль (т.к. нельзя самом вмешаться в этот процесс), а, во-вторых, сделано через одно место как раз из-за того, что не через классы.
Вы видимо не прочитали соседние комментарии в которых MaZaAa привел
пример реализации тех же хуков на классах, а также я пишу о том,
как можно было еще реализовать простое переиспользование
Такое ощущение, что MobX это какая то не актуальная дичь из далекого прошлого. Используя функциональные компоненты и context из коробки react все написанное можно сократить в 2 раза и упростить в 5 ... Если кто то ищет технологию для архитектуры приложения своего, то рассмотрите вариант использования context из коробки .
React + Mobx: в чём смысл?