Comments 42
Как пример условная кнопка «Написать (какое-то особое) письмо» которая создаёт особой формы форму (извиняюсь за тавтологию) с предзаполненными дефолтными значениями полями.
В тоже время иммутабельный подход исключает такое поведение в принципе.
Что касается 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 (не к ночи будь помянут).
Но сама идея библиотеки точно такая же.
… мне кажется плохой идеей подменять примитивы какими-то своими объектами. Например в 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)
Первый недостаток — это то, что мы не можем теперь просто взять и обновить любое свойство объекта данных в приложении. Из-за требования возвращать каждый раз новый иммутабельный объект целого состояния, нам нужно вернуть новый объект и также пересоздать все родительские объекты и массивы. Например, если объект состояния хранит массив проектов, каждый проект хранит массив задач, и каждая задача хранит массив комментариев:
Это как бы суть иммутабельности
Этот вариант уменьшает количество работы чтобы обновить объект, но с точки зрения производительности проблема остается. В нормализованном виде, если я правильном вас понял, у нас вместо дерева будет только два уровня — объект 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))
Что, на мой взгляд важнее, нормализация решает проблему дупликации данных...
… порожденную иммутабельностью. В мутабельном мире есть намного более простые решения этой проблемы.
return state
.setIn(['app', 'currentItemId'], action.id)
.setIn(['app', 'currentDirection'], 'down')
.setIn(['subtasks', subtask.id, 'top'], newTop)
.setIn(['subtasks', subtask.id, 'left'], newLeft);
Мемоизация селекторов вряд ли поможет, т.к. в них нету тяжелых расчетов.
Плюс Mobx что там такой функционал уже есть из коробки в mobx-state-tree.
А что не так с чистыми функциями?
Вредная статья в стиле "сам себе придумал проблему, сам решил". Про нормализацию стейта вообще странно, автор будто даже официальную документацию реакта не удосужился прочитать.
Как работает mobx изнутри и сравнение его с redux