Обновить

Комментарии 11

Борьба с ветряными мельницами, в результате которой родился очередной redux. Как способ ознакомления с flux — ок, как стейт-менеджер — мертворожденный. Имхо

Статья — про путь, а не заявка на место в рейтинге менеджеров. Мне хотелось пройти его самому и подробно описать — это и было целью.
Сравнение с redux, кстати, скорее из-за того, что я начинаю разбор именно с flux, итоговое API ближе к zustand.

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

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

Например, могли бы почитать MobX в 50 строчек кода - там очень похожее объяснение, но эта базовая реализация не делает инструмент удобным для прода. Нужно пройти еще годы развития и итераций, чтобы можно было брать в реальные проекты - без лишних .get() и .set(), с глубокой реактивностью, поддержкой классов, удобным подключением без counter.use("count") на каждую переменную, оптимизацией графа и т.п.

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

Про «фатальный недостаток» — отчасти так и есть, и я написал об этом в первых абзацах: хотел разобраться, как это устроено, а не заменить существующее. Статья про путь, библиотека оказалась побочным результатом.

Уточню одно: код в статье учебный, он нарочно упрощён ради объяснения, и это не то, что лежит в пакете. В библиотеке подписка по ключам, батчинг внутри действий, типы, выводимые из конфига и возможность указать источник у каждого обновления — благодаря чему persist не ловит собственное эхо, без флагов вроде isHydrating.

Про .use("count") на каждую переменную — там есть useSelector, который сам отслеживает, какие ключи прочитал, список зависимостей передавать не нужно.

С MobX сравнивать не возьмусь: там прокси и глубокая реактивность, здесь неизменяемые обновления и подписка на конкретные ключи. Это не недоделанный MobX, а другая модель со своими ограничениями.

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

Вы сделали zustand. Посмотрите его исходники, там тоже паттерн наблюдатель и все.

Но вы еще и батчите, а уверены что это нужно? Например как считать в вашем стейтменеджере кол-во изменений состояния?

Стейт менеджер это не про получение и установку состояния, это про реактивность, умную подписку на конкретные поля, DX и прочее

Про наблюдателя согласен — это база, и у zustand, и почти у всех подобных менеджеров. Разница в том, где отсекается лишнее: у zustand уведомляются все слушатели, а рендер отсекает селектор; здесь подписчики лежат в Map<ключ, Set>, и подписчик на одно поле не вызывается вообще, когда меняется соседнее.

Про батчинг: он не глобальный — включается внутри экшена и при одном set с несколькими ключами. И главное, склеиваются уведомления, а не изменения: middleware вызывается на каждый set до батча. Посчитать количество изменений можно ровно так:

let changes = 0;
nexus.middleware(() => { changes++; });

Три set внутри экшена дадут changes === 3 и одно уведомление подписчику.

Вопрос хороший, спасибо — я как раз поэтому и протащил контекст обновления через middleware, а не только через подписчиков.

Огромное спасибо Вам за статью! Ряд моментов по стейт-менеджерам в голове наконец-то уложились.

Очень рад что статья оказалась вам полезной!

Меня больше всего заинтересовал момент с useSelector и автоматическим определением зависимостей. Если я правильно понял идею, стор должен понимать, какие именно поля были прочитаны селектором, и обновлять компонент только при их изменении.

Интересно, как это работает с селекторами, где зависимости меняются динамически. Например, когда сначала читается selectedId, а потом items[selectedId]. Набор зависимостей в таком случае может меняться после каждого обновления.

Если зависимости действительно пересобираются при каждом выполнении селектора, было бы интересно увидеть в статье небольшой разбор этого механизма и сравнение по накладным расходам с обычной подпиской Zustand. На мой взгляд, это как раз одна из тех деталей, по которой такой стейт-менеджер можно оценивать не как очередную реализацию get/set, а именно как систему реактивности.

Это очень интересный вопрос заставивший меня сделать дополнительный проверки.

Да, всё верно: набор зависимостей пересобирается на каждом запуске селектора. Селектор получает не само состояние, а обёртку над ним, которая запоминает, к каким полям он обратился — эти поля и становятся подпиской. Если на следующем запуске набор оказался другим, компонент переподписывается.

Но ваш пример — как раз случай, где набор не меняется. Запоминаются только поля верхнего уровня, а items[selectedId] обращается к двум таким полям, items и selectedId, при любом значении selectedId. Набор меняется в другой ситуации — когда условие выбирает между разными полями: s.mode === "a" ? s.a : s.b. Тут зависимости переезжают с mode + a на mode + b, и подписка пересобирается. Есть и третий случай: если селектор перебирает состояние целиком (спред, Object.keys), отследить конкретные поля нельзя, и подписка честно становится «на всё».

По накладным решил не гадать, а померить на zustand 5.0.15. 100 компонентов на 50 полей, 20 обновлений, каждое меняет одно поле: у zustand 2120 запусков селектора, у меня 200. Рендеров при этом поровну — 40 и 40. То есть zustand не перерисовывает лишнего, он лишнего считает: селектор запускается у всех подписчиков, а сравнение результата отсекает рендер.

А если наоборот — хочется видеть каждое обновление целиком, как при подписке на весь стор в zustand, — для этого есть middleware. Он вызывается на каждый set, до склейки обновлений и независимо от того, кто на какие поля подписан:

nexus.middleware((prev, next, context) => {
  // вызовется на каждый set, даже если значение не изменилось
});

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

И честная оговорка: на маленьких сторах я проигрываю. На одном-трёх полях zustand быстрее примерно в полтора раза — раскладывать подписчиков по ключам дороже, чем обойти короткий список. Перелом примерно на десяти.

Замеры в статью добавлю, спасибо, это и правда стоит показать.

Очень интересная статья, спасибо за то, что освещаешь эту тему, не везде можно найти столь подробную статью по данной теме. Отдельная любовь - иллюстрации, так гораздо увлекательнее читать 😍

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации