Но надо добавить что этот LIS-алгоритм нужен если вам захотелось оптимизаций. Тот же реакт, несмотря на то что многие думают что у него есть какой-то там diff-алгоритм, ничего такого не имеет — там простое сравнение по ключу и перемещение несовпавшего элемента на текущее место. Из-за чего вместо одной операции получаем n-1 операций insertBefore если переместить последний элемент наверх (или первый в самый низ, точно не помню). В принципе если перемещение одного элемента еще можно оптимизировать добавив while-цикл c пропуском одинаковых элементов на концах то при перемещениях двух и больше элементов только lis-алгоритм будет гарантировать минимальное количество insertBefore операций
Благо, задача построения diff-а в программировании довольно известная — гуглится, внезапно, по словам нахождение наибольшей общей подпоследовательности. Решается она динамическим программированием. Алгоритм имеет квадратичную сложность.
Вы не до конца разобрались в этой теме «диффинга». LCS который имеет O(n^2) сводится (для случая когда элементы не повторяются) к LIS (longest increasing subsequence) которая имеет сложность n*log(n) и реализуется как часть «терпеливой» сортировки. Алгоритм примерно такой — создаем временный массив и проходимся по элементам нового массива — вытаскиваем элемент по ключу «key» и смотрим какой был индекс в предыдущем массиве. Если индекс больше индекса элемента который мы обработали до него то добавляем его в конец этого временного массива и в отдельном свойстве «prev» записываем элемент который стоит перед ним в временном массиве а если индекс меньше — то ищем бинарным поиском элемент с наименьшим индексом который больше нашего. По достижению конца списка смотрим на последний элемент во временном массиве и проходимся связанному списку «prev» — это и будет наибольшая возрастающая последовательность. Кстати в отличии от inferno, ivi и прочих этот алгоритм можно еще сильнее оптимизировать если не создавать новые временные массивы а переиспользовать один глобальный каждый раз при diff-е и уменьшить количество проходов — на первом проходе формируем тот временный массив а на обратном сразу можно предпринять перестановку элементов — не дожидаясь формирования конечного lis — то есть считываем свойство «prev» — если элемент равен текущему элементу в новом массиве то его не трогаем. Если не равен то нужный элемент вставляем в текущее место через insertBefore.
Мы не сможем на среднем ноутбуке рассчитывать весь экран по одному пикселю и иметь при этом хотя бы 30fps, не говоря уже о 60
Вы ошибаетесь, вы выбрали неправильную технологию — с 2d канвасом оно конечно будет дико тормозить. Но если взять webgl то все будет летать и можно хоть каждый пиксель экрана вычислять на 60 fps в шейдере. А все потому что в случае с 2d-контекстом канваса браузеру нужно отрендерить все пиксели на процессоре а потом передать весь массив пикселей на видеокарту, в то же время с webgl браузеру нужно передать на видеокарту только данные а все вычисления и рисование пикселей происходит на видеокарте. Вот пример вычисления каждого пикселя в шейдере, а здесь полноценные частицы с логикой
Спасибо, разобрался. А какой размер этого буфера или пакета? Его можно настраивать? Я правильно понимаю, что поскольку каждый запрос на запись ждет других запросов пока не заполнится буфер, то чем меньше размер буфера то будет быстрее каждый отдельный запрос но будет больше fsync-ов, а значит более медленная скорость записи на диск? А чем больше размер буфера то меньше fsynс-ов и быстрее сброс на диск но тем дольше будет ждать каждый отдельный запрос (пока не заполнится буфер) и тем самым будет занимать оперативку (а миллион ожидающих tcp-коннектов то займет не один гигабайт памяти)?
Спасибо, теперь понятно. То есть все равно получается один fsync на множество записей, но поскольку каждый запрос на запись ждет других запросов пока не заполнится буфер, то потеря этих данных не страшна для клиента который пишет что-то в базу как часть транзакции так как управление не возвращается
Сам тарантул их там группирует в пакеты и делает один fsync на несколько
Группировка пакетов и вызов одного fsync на несколько пакетов это и есть буфер и если внезапно выключится сервер то потеряется не последняя запись (в случае вызова fsync после каждой записи) а несколько
Как-то сильно сомневаюсь что тарантул делает честный fsync после каждых 220 байт. Я тут решил провести эксперимент на ноде
const fs = require('fs');
const N = 1000000;
const BufferLength = 1024*100
const fd = fs.openSync(__dirname+'/db.db', 'a');
const data = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
console.log('---start');
const time = Date.now();
let buffer = '';
for(let i = 0; i < N; i++){
buffer += data
if(buffer.length > BufferLength){
fs.writeSync(fd, buffer, null, 'utf8');
buffer = '';
//fs.fsyncSync(fd)
}
}
if(buffer.length){
fs.writeSync(fd, buffer, null, 'utf8');
}
const timeEnd = Date.now()-time;
console.log('time', timeEnd);
При буфере в 100кб происходит запись 1млн 220байтных строк у меня где-то за 840мс. При уменьшении буфера до 1кб — за 3.5 секунд. При нулевом буфере (вызов записи после каждых 220 байт) — 13-15с. Но это только систный вызов записи на диск. А ос имеет еще свой кеш или буфер и для того чтобы мы могли быть уверенны что записали 220 байт на диск нужно вызвать fsync и ос тогда сбросит этот буфер на диск. И если я откомментирую строчку fs.fsyncSync(fd) то производительность падает на пару порядков — запись 1000 строк по 220байт с нулевым буфером у меня занимает целых 6 секунд!
Поскольку данные в WAL-лог только пишутся, пишутся в режиме append, то таким способом можно утилизировать практически 100 % пропускной способности записи диска.
Что-то не верится в такую скорость работы базы данных. На сколько я знаю утилизировать 100% пропускной способности записи на диск можно только через буфер а это значит что мы жертвуем надежностью и после внезапного выключения сервера мы потеряем n записей которые мы якобы записали на диск отправив клиенту 200 ok.
Бенчмарки Тарантула на средненьком ноутбуке (2012 года выпуска) на размере сообщения 220 байт показывают производительность 160—180 тыс. записей в секунду, что в полтора-два раза больше, чем нам требуется для данной задачи.
А можно больше технических подробностей? Выполняется ли fsync после каждой записи в 220 байт? Если нет, то какой размер буфера (чтобы понимать сколько данных потеряются при выключении сервера) и какая тогда будет реальная производительность базы данных при fsync после каждой записи ?
Ваш пример на mobx будет выглядеть намного короче и удобней:
const Store = new class {
@observable booksList = [];
@observable selectedUser = new class {
@observable id = 1;
@observable name = "Yura",
@observable bestFriend = 1,
@observable additionalInfo = new class {
@observable age = 17
}
}
}
@observer
class AddtitionalUserInfo extends Component {
handleUpdateUserAge = (e) => {
Store.selectedUser.additionalInfo.age = 20;
};
render() {
return <div>
<div>User age: {Store.selectedUser.additionalInfo.age}</div>
<button onClick={this.handleUpdateUserAge}>Update user age</button>
</div>
}
}
1) Нет болерплейта и ручных подписок на стор в componentWillMount и componentWillUnmount
2) Нет неудобной записи с получением значения через строку this.uiState.getStoreData('selectedUser.additionalInfo.age') — мы можем прямо через стандартный js и точку обратится к любому свойству Store.selectedUser.additionalInfo.age
3) Нет неудобной записи с обновлением свойства Store.update('selectedUser.additionalInfo.age', 20); — разве не удобней записать обновление через свойства Store.selectedUser.additionalInfo.age = 20 а не возиться со строкой пути?
4) Как вы поступите если вам нужно будет в вашем сторе сохранять древовидные комментарии которые могут быть бесконечно вложенными? Если у вас стор можно считывать или обновлять только через указание пути к данным от корня то компонентам потребуется дополнительно еще передавать другу другу путь отдельным пропсом, когда же в mobx можно просто передать объект например <div>comment.children.map(comment=><Comment comment={comment}/></div> а потом в компоненте комментария обновить данные прямо на объекте this.props.comment.text = e.target.value без возни с путями, независимо от глубины в котором хранится этот комментарий.
5) Получение и обновление свойств через стоковый путь в сторе лишает возможности протипизировать работу с состоянием и отловить ошибки на стадии компиляции.
В mobx есть только один момент которого стоит придерживаться — нужно чтобы свойство на которое будут подписываться компоненты было помечено декоратором @observable
В примере, я записал вложенный объект через создание анонимного класса потому что декоратор можно объявить только в классе. В реальном приложении обычно синглтон-объектов очень мало а в процессе работы будут создаваться объекты с разным набором свойств и тогда логично и правильно эти типы вынести в отдельные классы. Например объект selectedUser будет не литералом а создаем объекта класса User а объект вложенный объект additionalInfo будет объектом другого класса Profile.
const Store = new class {
@observable booksList = [];
@observable selectedUser = new User({id: 1, name: "Yura", bestFriend: 1, profile: {age: 17}})
}
class User {
@observable id;
@observable name = "Yura",
@observable bestFriend,
@observable additionalInfo;
constructor({id, name, bestFriend, profile}){
this.id = id;
this.name = name;
this.bestFriend = bestFriend;
this.profile = new Profile(profile);
}
}
class Profile {
@observable age;
constructor({age}){
this.age = age;
}
}
Тут примерно полная аналогия с таблицами в реляционных базах данных — там нельзя создать вложенный объект в таблице и для этого нужно создавать отдельную таблицу. На клиенте точно также — таблицы просто будут классами — User, Profile, Post, Comment и т.д где мы объявляем типы колонок (для статической типизации) и помечаем декоратором @observable свойства которые рендерим в компонентах
С точки зрения поддержки у него две беды: отсутствие уникального класса у каждого элемента и его неимоверная дубовость. Что мы будем делать, если на одной странице из 20 нам потребуется убрать подзаголовок, на другой добавить после него параграф с описанием, а на третьей выводить имена героев до идентификатора, а не после? Вариантов тут не очень много…
Итак, зачем нам вся эта свистопляска с кучей свойств? Дело в том, что каждое такое свойство — это точка расширения. Мы можем изменить в компоненте любой аспект поведения, просто переопределив соответствующее свойство.
На самом деле эту "уникальную" расширяемость $mol можно применить и к реакт-компонентам если вынести части верстки в методы чтобы потом можно было отнаследоваться и переопределить все что захочется.
Например, тот пример с панелями в вашей статье
$mol_pager $mol_viewer
sub /
< header $mol_viewer
sub < head /
< titler $mol_viewer
sub /
< title
< bodier $mol_scroller
sub < body /
< footer $mol_viewer
sub < foot /
можно было бы записать и на реакте с ничуть не хужей расширяемостью
и потом можно также переопределить все что захочется. Но так никто не делает. Можно сказать что с таким подходом есть проблема в том что много болерплейта и ручной работы а также ненаглядность иерархии композиции из-за чего нужно бродить глазами по методам. $mol это решает тем что вводит новый язык разметки и при этом автоматически генерирует такие классы за программиста, но тоже самое можно было решить добавив во всем знакомый html xml-неймспейсы, чтобы jsx парсер по названию неймспейса сам выносил в методы вложенные части верстки. Тогда бы получилось бы что-то вроде этого
То есть парсер, увидев ':', создаст и перенесет вложенную разметку в метод с таким именем создавая тем самым точку расширения которую можно переопределить через наследование в другом компоненте (template тег играет роль заглушки для метода с пустым содержимым)
Но так почему-то никто до сих пор не делает и на то скорее всего есть причина в том что в реакт-сообществе наследование, несмотря на гибкие возможности расширения, не приветствуется
Создал тут пример чтобы оценить влияние на производительность иммутабельного подхода 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
Этот вариант уменьшает количество работы чтобы обновить объект, но с точки зрения производительности проблема остается. В нормализованном виде, если я правильном вас понял, у нас вместо дерева будет только два уровня — объект AppState будет хранить список таблиц а каждая таблица будет хранить хеш объектов по их айдишнику
Теперь если нам нужно обновить комментарий то мы можем "просто" написать так 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))
Да, вы совершенно правы, mobx в @observer декораторе действительно переопределяет shouldComponentUpdate возвращая true если не изменились пропсы потому что вложенные компоненты будут обновляться отдельно. Я просто забыл добавить этот момент в статье. Более того для того чтобы не получилась ситуация когда мы обновляем более глубокий компонент раньше его родителя (в случае когда нужно обновить обоих) mobx использует специальный метод react-а ReactDOM.unstable_batchedUpdates который обновит компоненты в правильном порядке. В redux кстати существует точно такая же проблема и необходимо вручную добавлять middlerare с вызовом unstable_batchedUpdates (https://github.com/reactjs/redux/issues/1415)
Вы не до конца разобрались в этой теме «диффинга». LCS который имеет O(n^2) сводится (для случая когда элементы не повторяются) к LIS (longest increasing subsequence) которая имеет сложность n*log(n) и реализуется как часть «терпеливой» сортировки. Алгоритм примерно такой — создаем временный массив и проходимся по элементам нового массива — вытаскиваем элемент по ключу «key» и смотрим какой был индекс в предыдущем массиве. Если индекс больше индекса элемента который мы обработали до него то добавляем его в конец этого временного массива и в отдельном свойстве «prev» записываем элемент который стоит перед ним в временном массиве а если индекс меньше — то ищем бинарным поиском элемент с наименьшим индексом который больше нашего. По достижению конца списка смотрим на последний элемент во временном массиве и проходимся связанному списку «prev» — это и будет наибольшая возрастающая последовательность. Кстати в отличии от inferno, ivi и прочих этот алгоритм можно еще сильнее оптимизировать если не создавать новые временные массивы а переиспользовать один глобальный каждый раз при diff-е и уменьшить количество проходов — на первом проходе формируем тот временный массив а на обратном сразу можно предпринять перестановку элементов — не дожидаясь формирования конечного lis — то есть считываем свойство «prev» — если элемент равен текущему элементу в новом массиве то его не трогаем. Если не равен то нужный элемент вставляем в текущее место через insertBefore.
Вы ошибаетесь, вы выбрали неправильную технологию — с 2d канвасом оно конечно будет дико тормозить. Но если взять webgl то все будет летать и можно хоть каждый пиксель экрана вычислять на 60 fps в шейдере. А все потому что в случае с 2d-контекстом канваса браузеру нужно отрендерить все пиксели на процессоре а потом передать весь массив пикселей на видеокарту, в то же время с webgl браузеру нужно передать на видеокарту только данные а все вычисления и рисование пикселей происходит на видеокарте.
Вот пример вычисления каждого пикселя в шейдере, а здесь полноценные частицы с логикой
Группировка пакетов и вызов одного fsync на несколько пакетов это и есть буфер и если внезапно выключится сервер то потеряется не последняя запись (в случае вызова fsync после каждой записи) а несколько
У меня получилось где-то
Как-то сильно сомневаюсь что тарантул делает честный fsync после каждых 220 байт. Я тут решил провести эксперимент на ноде
При буфере в 100кб происходит запись 1млн 220байтных строк у меня где-то за 840мс. При уменьшении буфера до 1кб — за 3.5 секунд. При нулевом буфере (вызов записи после каждых 220 байт) — 13-15с. Но это только систный вызов записи на диск. А ос имеет еще свой кеш или буфер и для того чтобы мы могли быть уверенны что записали 220 байт на диск нужно вызвать fsync и ос тогда сбросит этот буфер на диск. И если я откомментирую строчку
fs.fsyncSync(fd)
то производительность падает на пару порядков — запись 1000 строк по 220байт с нулевым буфером у меня занимает целых 6 секунд!Что-то не верится в такую скорость работы базы данных. На сколько я знаю утилизировать 100% пропускной способности записи на диск можно только через буфер а это значит что мы жертвуем надежностью и после внезапного выключения сервера мы потеряем n записей которые мы якобы записали на диск отправив клиенту 200 ok.
А можно больше технических подробностей? Выполняется ли fsync после каждой записи в 220 байт? Если нет, то какой размер буфера (чтобы понимать сколько данных потеряются при выключении сервера) и какая тогда будет реальная производительность базы данных при fsync после каждой записи ?
Ваш пример на mobx будет выглядеть намного короче и удобней:
1) Нет болерплейта и ручных подписок на стор в
componentWillMount
иcomponentWillUnmount
2) Нет неудобной записи с получением значения через строку
this.uiState.getStoreData('selectedUser.additionalInfo.age')
— мы можем прямо через стандартный js и точку обратится к любому свойствуStore.selectedUser.additionalInfo.age
3) Нет неудобной записи с обновлением свойства
Store.update('selectedUser.additionalInfo.age', 20);
— разве не удобней записать обновление через свойстваStore.selectedUser.additionalInfo.age = 20
а не возиться со строкой пути?4) Как вы поступите если вам нужно будет в вашем сторе сохранять древовидные комментарии которые могут быть бесконечно вложенными? Если у вас стор можно считывать или обновлять только через указание пути к данным от корня то компонентам потребуется дополнительно еще передавать другу другу путь отдельным пропсом, когда же в mobx можно просто передать объект например
<div>comment.children.map(comment=><Comment comment={comment}/></div>
а потом в компоненте комментария обновить данные прямо на объектеthis.props.comment.text = e.target.value
без возни с путями, независимо от глубины в котором хранится этот комментарий.5) Получение и обновление свойств через стоковый путь в сторе лишает возможности протипизировать работу с состоянием и отловить ошибки на стадии компиляции.
В mobx есть только один момент которого стоит придерживаться — нужно чтобы свойство на которое будут подписываться компоненты было помечено декоратором
@observable
В примере, я записал вложенный объект через создание анонимного класса потому что декоратор можно объявить только в классе. В реальном приложении обычно синглтон-объектов очень мало а в процессе работы будут создаваться объекты с разным набором свойств и тогда логично и правильно эти типы вынести в отдельные классы. Например объект
selectedUser
будет не литералом а создаем объекта класса User а объект вложенный объект additionalInfo будет объектом другого класса Profile.Тут примерно полная аналогия с таблицами в реляционных базах данных — там нельзя создать вложенный объект в таблице и для этого нужно создавать отдельную таблицу. На клиенте точно также — таблицы просто будут классами — User, Profile, Post, Comment и т.д где мы объявляем типы колонок (для статической типизации) и помечаем декоратором
@observable
свойства которые рендерим в компонентахНа самом деле эту "уникальную" расширяемость $mol можно применить и к реакт-компонентам если вынести части верстки в методы чтобы потом можно было отнаследоваться и переопределить все что захочется.
Например, тот пример с панелями в вашей статье
можно было бы записать и на реакте с ничуть не хужей расширяемостью
и потом можно также переопределить все что захочется. Но так никто не делает. Можно сказать что с таким подходом есть проблема в том что много болерплейта и ручной работы а также ненаглядность иерархии композиции из-за чего нужно бродить глазами по методам. $mol это решает тем что вводит новый язык разметки и при этом автоматически генерирует такие классы за программиста, но тоже самое можно было решить добавив во всем знакомый html xml-неймспейсы, чтобы jsx парсер по названию неймспейса сам выносил в методы вложенные части верстки. Тогда бы получилось бы что-то вроде этого
То есть парсер, увидев ':', создаст и перенесет вложенную разметку в метод с таким именем создавая тем самым точку расширения которую можно переопределить через наследование в другом компоненте (
template
тег играет роль заглушки для метода с пустым содержимым)Но так почему-то никто до сих пор не делает и на то скорее всего есть причина в том что в реакт-сообществе наследование, несмотря на гибкие возможности расширения, не приветствуется
Тьху, я перепутал — в shouldComponentUpdate он возвращает
false
а неtrue
конечно же.Этот вариант уменьшает количество работы чтобы обновить объект, но с точки зрения производительности проблема остается. В нормализованном виде, если я правильном вас понял, у нас вместо дерева будет только два уровня — объект AppState будет хранить список таблиц а каждая таблица будет хранить хеш объектов по их айдишнику
Теперь если нам нужно обновить комментарий то мы можем "просто" написать так
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))
Да, вы совершенно правы, mobx в
@observer
декораторе действительно переопределяетshouldComponentUpdate
возвращаяtrue
если не изменились пропсы потому что вложенные компоненты будут обновляться отдельно. Я просто забыл добавить этот момент в статье. Более того для того чтобы не получилась ситуация когда мы обновляем более глубокий компонент раньше его родителя (в случае когда нужно обновить обоих) mobx использует специальный метод react-а ReactDOM.unstable_batchedUpdates который обновит компоненты в правильном порядке. В redux кстати существует точно такая же проблема и необходимо вручную добавлять middlerare с вызовом unstable_batchedUpdates (https://github.com/reactjs/redux/issues/1415)