Pull to refresh

Comments 42

Я сначала подумал, что это опечатка.

А я сначала подумал, что это некая ирония (DOM — DOOM), но уже после третьего "дум" начало резать глаза и мозг

Вот что мне не нравится в подходе с мутабельностью данных так это то, что это порождает целый класс ошибок. Например у нас есть некий объект в котором хранится начальное состояние приложения. Юзер взаимодействуя с приложением изменит этот объект и, если мы не предусмотрели очистку, то данные сохранятся в исходном объекте и могут попасть туда где им не положено быть.
Как пример условная кнопка «Написать (какое-то особое) письмо» которая создаёт особой формы форму (извиняюсь за тавтологию) с предзаполненными дефолтными значениями полями.
В тоже время иммутабельный подход исключает такое поведение в принципе.

Что касается mobx, то мне кажется плохой идеей подменять примитивы какими-то своими объектами. Например в mobx вместо Array используется ObservableArray который не пройдёт проверку на typeof someVariable == Array. С другой стороны, иного способа реализовать такое же поведение я не знаю. Может быть Proxy поможет, но я сильно в этом сомневаюсь.
С первым как-то не сталкивался. Просто пишу отдельные сторы для каждого «класса» данных, которые работают сами-посебе и о внешнем мире не знают: и вроде бы нет подобных проблем.

Для проверок просто использую самописную is_array() в которой проводится так же проверка на isObservableArray(). Тоже проблем не возникает.
Как будто иммутабельные данные очищать не нужно…
Может быть Proxy поможет

Vue 3-й версии как раз переписывают на Proxy (сейчас там все работает как в Mobx).
Так что особый ObservableArray это скорее всего временное решение, которое умрет вместе с IE.


Основная фишка MobX – это не пляски с геттерами-сеттерами, а реактивная парадигма. Вон, Knockout вообще использует obj.prop() как геттер и obj.prop(value) как сеттер.
Да, неудобно. Зато работает со времен IE 6 (не к ночи будь помянут).
Но сама идея библиотеки точно такая же.

Активно в проекте использую redux. Полюбился он мне. Многие ругают его за многословность и шаблонность. Поставил redux-actions и радуюсь жизни. Асинхронная логика лежит в саге, в редких случаях в middleware. Данные храню нормализовано, для выборок использую селекторы. Зато все прозрачно. Проблемы с дебагом возникают только в генераторах.
>>Проблемы с дебагом возникают только в генераторах.
Какого рода?

Либо я неправильно настроил babel, но построчное выполнение кода внутри генератора вообще уводит куда-то в сторону (redux-saga). Если какая-то ошибка, никакие source-map'ы не помогают. Стектрейс вообще неадекватный в консоли.

… мне кажется плохой идеей подменять примитивы какими-то своими объектами. Например в mobx вместо Array используется ObservableArray

Так Array это и не примитив


… вместо Array используется ObservableArray который не пройдёт проверку на typeof someVariable == Array

А что вообще пройдет такую проверку, если typeof [] === 'object"?


Не холивара ради, просто правда не понял этих аргументов

Вероятно, имелось в виду что-то такое, а не typeof:


Array.isArray([1, 2, 3]);  // true
Array.isArray({foo: 123}); // false
Array.isArray('foobar');   // false
Array.isArray(undefined);  // false
Несмотря на то что react изобрел виртуальный дум со слоганом что реальный дум медленный, а виртуальный быстрый потому что он сравнивает только деревья объектов в памяти, а в реальном думе обновляет только измененные части, в реальности мы не можем при любом обновлении данных в приложении вызвать это сравнение виртуального дума для всего приложения потому что это медленно.

Это не так, если использовать иммутабельность, чистые функции и мемоизацию (для исключения перерендера самого VDOM когда исходные данные не поменялись). С иммутабельностью достаточно легко перендерить только поменявшиеся части, достаточно сравнить ссылки при траверсинге структуры вглубь!


Подробнее можно почитать в моей статье на хабре.

Достаточно чистых функций и мемоизации, иммутабельность не обязательна. :-)
Если мы определили список компонентов, которые необходимо обновить, то также нужна их иеархичность:
например компонент A содержит в себе компонент B, который содержит в себе компонент C.
Если вдруг оказалось, что надо обновить все эти 3 компонента — то важна последовательность, т.к. нет смысла запускать обновление компонента С вначале, т.к. вполне может оказаться так, что в процессе обновления компонента B компонент C просто будет удален.
А теперь другой вариант — если компонент A отмечен к обновлению, но изменений в компоненте B не замечено, то при обновлении компонента A будет обновлен также и компонент B (не знаю точно, реализовано ли в mobx прослеживание этой ситуации в функции shouldComponentUpdate компонента B)

Да, вы совершенно правы, mobx в @observer декораторе действительно переопределяет shouldComponentUpdate возвращая true если не изменились пропсы потому что вложенные компоненты будут обновляться отдельно. Я просто забыл добавить этот момент в статье. Более того для того чтобы не получилась ситуация когда мы обновляем более глубокий компонент раньше его родителя (в случае когда нужно обновить обоих) mobx использует специальный метод react-а ReactDOM.unstable_batchedUpdates который обновит компоненты в правильном порядке. В redux кстати существует точно такая же проблема и необходимо вручную добавлять middlerare с вызовом unstable_batchedUpdates (https://github.com/reactjs/redux/issues/1415)

Тьху, я перепутал — в shouldComponentUpdate он возвращает false а не true конечно же.

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

Это как бы суть иммутабельности

UFO just landed and posted this here

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


let AppState = {
   folders: {
     ....,
     '11': {
        id: '11',
        name: 'folder1',
        projects: ['34', '42']
     }
   },
   projects: {
       ...
       '34': {
          id: '34',
          title: 'project1',
          tasks: ['112', '213'],
          folder: '11'
       }
   },
   tasks: {
      '112': {
          id: '112',
          text: 'task1'
          comments: ['211'],
          project: '34'
     }
  },
  comments: {
      '211': {
        id: '211',
        text: 'comment1',
        parent: null,
        task: '112'
     }
  }
} 

Теперь если нам нужно обновить комментарий то мы можем "просто" написать так AppState = {...AppState, comments: {...AppState.comments, [comment.id]: {...comment, text: 'new text'}}}
А с точки зрения производительности мы все равно выполняем кучу работы — а) создаем новый объект комментария и копируем туда остальные свойства, б) создаем новый объект AppState и копируем туда все остальные таблицы в) — это самое важное — создаем новый объект хеша и копируем туда все айдишники со ссылками на другие объекты. В варианте с деревом количество комментариев которые надо скопировать ограничивался только одним таском (а их обычно немного) то теперь мы совместили все комментарии всех тасков всех проектов со всех папок в одном большом хеше и нам теперь придется их все копировать каждый раз при обновлении. Можно сказать что это микроптимизации приведя цитату Кнута, но когда вы столкнетесь с высокочастотными обработчиками событий вроде перемещения мыши или скролла этот подход с копированием тысячи айдишников и созданием лишних объектов (особенно когда redux-у при любом обновлении стора нужно вызвать mapStateToProps абсолютно всех подключенных компонентов, а что мы делаем внутри mapStateToProps? — мы создаем новый объект указывая дополнительные пропсы) будет вызывать тормоза. Поэтому тут подход с обсерверами mobx выигрывает потому что у нас: a) не будет создан ни один лишний объект б) обновление свойства любого объекта все равно короче и проще — comment.text = 'new text';


Но основная проблема с нормализованным подходом в другом — мы теряем возможность обращаться к другим частям состояния просто обращаясь по ссылке. Поскольку связи мы теперь моделируем через айдишники, то каждый раз когда нам нужно обратится к родительской сущности или вложенным сущностям нам нужно каждый раз вытаскивать объект по его айдишнику из глобального стора. Например, когда нужно узнать рейтинг родительского комментария мы не можем просто написать как в mobx comment.parent.rating — нам нужно вытащить объект по айдишнику — AppState.comments[comment.parentId].raiting. А как мы знаем ui может быть сколь угодно быть разнообразным и компонентам может потребоваться информация о различных частях состояния и такой код вытаскивания по айдишнику на каждый чих легко превращается в некрасивую лапшу и будет пронизывать все приложение. Например, нам нужно узнать самый большой рейтинг у вложенных комментариев, то вариант с обсерверами и ссылками между объетами — comment.children.sort((a,b)=>b.rating - a.rating))[0] а в варианте с иммутабельностью и айдишниками нужно еще дополнительно замапить айдишники на объеты — comment.children.map(сhildId=>AppState.comments[childId]).sort((a,b)=>b.rating - a.rating))[0]. Или вот, сравните пример когда у нас есть объект комментария нужно узнать имя папки в котором он находится: 1) — вариант c ссылками — comment.task.project.folder.name 2) вариант с айдишниками — AppState.folders[AppState.projects[AppState.tasks[comment.taskId].projectId].folderId].name
И с точки зрения производительности — операция получения объекта по ссылке это O(1), а операция вытаскивания объекта по айдишнику это уже O(log(n))

UFO just landed and posted this here
Что, на мой взгляд важнее, нормализация решает проблему дупликации данных...

… порожденную иммутабельностью. В мутабельном мире есть намного более простые решения этой проблемы.

UFO just landed and posted this here
Зачем туда класть дубликаты объектов, когда можно положить тот же самый объект?
UFO just landed and posted this here

Структура транспортного сообщения не обязана совпадать с структурой вью-модели.


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

UFO just landed and posted this here
Создал тут пример чтобы оценить влияние на производительность иммутабельного подхода redux-а — codesandbox.io/s/7yvmx50m06 против обсерверов mobx-а codesandbox.io/s/1qrvz4qp57. При клике на любой subtask будет происходить анимация движения подзадачи по экрану. На 11 тысячах подключенных компонентов фпс (инструмент в chrome devtools) в примере с redux у меня где-то 20, а если взять продакшн-сборку реакта и redux то больше 30 не поднимается. У mobx стабильно 59. Советую сделать замер на вкладке performance в chrome devtools и посмотреть гребешки выделения памяти. 11 тысяч компонентов это конечно много но это когда уже тормозит а значит для 60 фпс надо не больше 5 тысяч. А учитывая что обработчики могут быть посложнее обновления свойства то на обновление компонентов останется еще меньше времени а если еще будет несколько обработчиков высокочастотных событий или анимаций одновременно (а это частый случае потому что компоненты хочется делать независимыми а не шарить один обработчик анимации на всех) то падение производительности будет еще больше и количество компонентов надо еще больше уменьшить. Но это на компьютере, а если взять мобилки с медленным процессором и небольшой памятью то ситуация будет еще хуже. Может я чего-то пропустил но я даже не вижу способов что-то закешировать, замемоизировать или что там еще можно сделать с иммутабельностью в примере с redux
UFO just landed and posted this here
В примере с redux для мутаций используются spread-ы. Они достаточно медленные по сравнению с мутациями в immutable js, и годятся только для небольших проектов. Да и читаемость мутаций в immutable лучше:

return state
  .setIn(['app', 'currentItemId'], action.id)
  .setIn(['app', 'currentDirection'], 'down')
  .setIn(['subtasks', subtask.id, 'top'], newTop)
  .setIn(['subtasks', subtask.id, 'left'], newLeft);

Мемоизация селекторов вряд ли поможет, т.к. в них нету тяжелых расчетов.
Описанные проблемы успешно решаются с помощью нормализации данных. Перед тем как ложить данные в store, они разбиваются на мелкие сущности с помощью normalizr, и дальше универсальным редьюсером добавлются в стор. Если нужно отобразить объект в компоненте, то он денормализуется с помощью denormalize в каком-нибудь селекторе. При редактировании объекта лучше использовать нормализованную форму.

Плюс Mobx что там такой функционал уже есть из коробки в mobx-state-tree.
Уход от функционального программирования, иммутабельности и чистых функций. Я уже молчу про магию и неявную логику обновления. Я бы не рискнул использовать подобное в крупных коммерческих проектах.

А что не так с чистыми функциями?

Вот кстати, @computed в Mobx – именно что чистые функции. Иначе работать не будет.

Не чистые (зависят от изменяемых свойств), но идемпотентные (при неизменных зависимостях дают неизменный результат).

Да, действительно. Как-то упустил этот момент

Скажу так, для каждого @computed можно составить чистую функцию которую оно реализует — но в коде она не представлена.

Весь веб раньше писался без функционального программирования, и сейчас добрая половина крупных коммерческих проектов пишется на классическом ООП без функциональных плюшек, оставшиеся — с элементами функционального программирования, но вы всё равно не сможете написать всё приложение используя только ФП, можно вынести только какие-то части программы. Практически все GUI нативных программ написаны не на ФП, браузер, операционная система, почти всё, что вы видите. ФП надёжно зарекомендовал себя на практике только в математическом софте, некоторых типах серверов и специальных приложений, использовать ФП в UI — сравнительно новая идея. Скорее можно побояться использовать, а не не использовать ФП в коммерческих проектах, так как не смотря на широкое распространение в последние годы, такой подход часто проигрывает по скорости разработки и не сильно меняет количество багов. По опыту нашей команды, после перехода с Redux на MobX, скорость разработки выросла ориентировочно в 3 раза.

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

Sign up to leave a comment.

Articles

Change theme settings