Стратегии для неизменного JavaScript
Неизменяемые данные — это концепция, с которой мне пришлось столкнуться при написании моего первого редьюсера для ngrx/store. Идея неизменяемых данных заключается в том, что их нельзя изменить после создания. Этот простой сдвиг в управлении состоянием может упростить создание и отладку приложений. Имея единственный источник достоверной информации, вы получаете контракт, который избавит вас от целого класса проблем, с которыми сталкиваются изменяемые приложения.
Я поделюсь тем, что я узнал на данный момент. Имейте в виду, что эти примеры основаны на моем опыте использования ngrx/store, но никоим образом не связаны с ним напрямую.
Давайте начнем с некоторого состояния: это состояние будет состоять из объекта с массивом идентификаторов и объекта сущностей, который будет хранить один ключ/значение для каждого идентификатора в массиве идентификаторов.
let states = []; const initialState = { ids: [], entities: {} } states = [...states, initialState]
Добавление некоторых данных в наше пустое состояние:
// add john const john = {id: 1, name: 'John Lennon'}; const solo = { ids: [...initialState.ids, john.id], entities: Object.assign({}, initialState.entities, {[john.id]: john}) } states = [...states, solo];
Здесь многое происходит. Мы рассмотрим одну вещь за раз. john
— это новый объект, который мы хотим добавить в состояние.
const john = {id: 1, name: 'John Lennon'};
Из объекта john
мы извлечем свойство id, чтобы добавить его в массив ids
, и скопируем свойства id и name в объект entities
.
const solo = { ids: [] //add new ids, entities: {} //add new entities }
Добавление новых идентификаторов. Оператор распространения используется для расширения любых существующих идентификаторов, за которыми следует идентификатор нового объекта. В массиве solo.ids
не будет ссылок на массив initialState.ids
.
ids: [...initialState.ids, john.id]
Добавление новых сущностей. Метод Object.assign
используется для создания нового объекта сущностей. Первый аргумент — это вновь созданный объект, за которым следует initialState.entities
. Ключ/значение из initialState.entities
будут поверхностно скопированы в новый пустой объект. Наконец, object{[john.id]: john}
будет объединен с вновь созданным объектом.
entities: Object.assign({}, initialState.entities, {[john.id]: john})
В итоге состояние solo
будет выглядеть так.
{ ids: [1], entities: { 1: {id: 1, name: 'John Lennon'} } }
Добавление дополнительных данных. Массив others
содержит новые объекты, которые нужно добавить в состояние. Нам нужно извлечь идентификаторы из массива и свести объекты в нем к одному объекту.
// add paul, george and ringo const others = [ {id: 2, name: 'Paul McCartney'}, {id: 3, name: 'George Harrison'}, {id: 4, name: 'Ringo Starr'} ]; let newIds = others.map(x => x.id); let newEntities = others.reduce((acc, member) => { return Object.assign(acc, {[member.id]: member}); },{}) const fabFour = { ids: [...solo.ids, ...newIds], entities: Object.assign({}, solo.entities, newEntities) }; states = [...states, fabFour];
Сначала идентификаторы: newIds = others.map(x => x.id)
создает новый массив с именем newIds
, который не имеет ссылки на массив others
. Затем объедините newIds
с solo.ids
, используя оператор распространения следующим образом. ids: […solo.ids, …others.map(x => x.id)]
.
Сущности требуют немного больше работы: reduce
и Object.assign
используются вместе для неглубокого копирования всех членов массива others
в один новый объект с именем newEntities
. Как и прежде, ссылок на старые данные нет. Новые данные скопированы. Никаких мутаций не произошло.
let newEntities = others.reduce((acc, member) => { return Object.assign(acc, {[member.id]: member}); },{})
Сборка состояния. Это так же просто, как копирование существующих данных и новых данных в новый объект. Оператор распространения используется для расширения существующих solo.ids
и newIds
, которые объединяются в массив fabFour.ids
. Object.assign
используется для объединения существующих пар ключ/значение solo.entities
и newEntities
в новый пустой объект {}
.
const fabFour = { ids: [...solo.ids, ...newIds], entities: Object.assign({}, solo.entities, newEntities) };
Добавление дополнительных данных. Имея некоторые данные в состоянии, давайте еще раз взглянем на добавление нового объекта. Это должно выглядеть очень знакомо. Это тот же процесс, который использовался для добавления john
к более раннему состоянию.
//add billy const billy = {id: 5, name: 'Billy Preston'}; const fabFourV1 = { ids: [...fabFour.ids, billy.id], entities: Object.assign({}, fabFour.entities, {[billy.id]: billy}) }
Удаление и замена данных. Давайте удалим billy
и заменим его на eric
. Стратегии такие же, как мы использовали до сих пор. Сначала отфильтруйте идентификаторы до newIds
. newIds
больше не будет содержать billy.id
. Затем уменьшите newIds
до объекта newEntities
, ища каждый элемент из массива newIds
в объекте fabFourV1.entities
. В конце концов newEntities
больше не будет содержать объект billy
. Наконец, объедините eric.id
с отфильтрованным newIds
и объедините eric
с отфильтрованным newEntities
.
//remove billy and replace him with eric const eric = {id: 6, name: 'Eric Clapton'}; newIds = fabFourV1.ids.filter(x => x != billy.id) newEntities = newIds.map(id => fabFourV1.entities[id]).reduce((acc, member) => { return Object.assign(acc, {[member.id]: member}); },{}); fabFourV2 = { ids: [...newIds, eric.id], entities: Object.assign({}, newEntities, {[eric.id]: eric}) }
В этом упражнении было создано 5 состояний; initialState
, solo
, fabFour
, fabFourV1
, fabFourV2
. Каждое состояние было создано с помощью неизменяемых методов. Между ними нет общих ссылок.
for (var i = 0, len = states.length; i < len; i++) { console.log(states[i].ids.map(id => states[i].entities[id].name)); }
Этот вывод показывает, что каждое состояние является отдельным объектом.
[] ["John Lennon"] ["John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr"] ["John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr", "Billy Preston"] ["John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr", "Eric Clapton"]
Во время создания этой статьи состояния не изменялись. Полный исходник можно найти здесь.
Если вам это понравилось, нажмите 💚 ниже, чтобы другие люди увидели это здесь, на Medium.