Как стать автором
Обновить

Комментарии 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 конечно же.

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

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

НЛО прилетело и опубликовало эту надпись здесь

Этот вариант уменьшает количество работы чтобы обновить объект, но с точки зрения производительности проблема остается. В нормализованном виде, если я правильном вас понял, у нас вместо дерева будет только два уровня — объект 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))

НЛО прилетело и опубликовало эту надпись здесь
Что, на мой взгляд важнее, нормализация решает проблему дупликации данных...

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

НЛО прилетело и опубликовало эту надпись здесь
Зачем туда класть дубликаты объектов, когда можно положить тот же самый объект?
НЛО прилетело и опубликовало эту надпись здесь

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


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

НЛО прилетело и опубликовало эту надпись здесь
Создал тут пример чтобы оценить влияние на производительность иммутабельного подхода 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
НЛО прилетело и опубликовало эту надпись здесь
В примере с 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 раза.

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

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

Публикации

Истории