Стратегии для неизменного 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.