Документация immer.js и практика в проектах Redux

JavaScript React.js Immutable.js

статья вgithubОткрытый исходный код, добро пожаловать, Fork, Star!

предисловие

ImmerЭто неизменяемая библиотека, написанная автором mobx.Основная реализация заключается в использовании прокси-сервера ES6, который реализует неизменяемую структуру данных js практически с минимальными затратами.Он прост в использовании, имеет небольшой размер и оригинальный дизайн. Требования к структуре данных.
К сожалению, в Интернете слишком мало полных документов, поэтому я написал один сам.Эта статья дает исчерпывающее объяснение Immer с идеями и процессами, близкими к реальному бою.

Проблемы с обработкой данных

Сначала определите исходный объект для использования в последующих примерах: Сначала определитеcurrentStateОбъекты, переменные используются в более поздних примерахcurrentStateКогда нет специальной декларации, это относится к этомуcurrentStateобъект

let currentState = {
  p: {
    x: [2],
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/8/17293174bf805c43~tplv-t2oaga2asx-image.image)
  },
}

При каких обстоятельствах исходный объект может быть случайно изменен?

// Q1
let o1 = currentState;
o1.p = 1; // currentState 被修改了
o1.p.x = 1; // currentState 被修改了

// Q2
fn(currentState); // currentState 被修改了
function fn(o) {
  o.p1 = 1;
  return o;
};

// Q3
let o3 = {
  ...currentState
};
o3.p.x = 1; // currentState 被修改了

// Q4
let o4 = currentState;
o4.p.x.push(1); // currentState 被修改了

Решение модификации объекта ссылочного типа

  1. Глубокое копирование, но стоимость глубокого копирования высока, что повлияет на производительность;
  2. ImmutableJS, очень хорошая библиотека неизменяемых структур данных, может решить вышеуказанные проблемы, но, по сравнению с Immer, ImmutableJS имеет два основных недостатка:
  • Пользователи должны изучить его метод работы со структурой данных, а метод работы с использованием собственных объектов, предоставляемых Immer, прост и удобен в использовании;
  • Результат его работы должен пройтиtoJSТолько метод может получить собственный объект, что заставляет при работе с объектом всегда обращать внимание на то, является ли операция собственным объектом или возвращаемым результатом ImmutableJS.Если вы не будете осторожны, возникнут непредвиденные ошибки.

Похоже, нас не устраивают известные на данный момент решения, так в чем же гениальность Иммера?

введение функции погружения

установить иммерс

Если вы хотите делать хорошие вещи, вы должны сначала отточить свои инструменты, и установка Immer является первоочередной задачей.

npm i --save immer

Как immer решает эти неприятные проблемы

Исправить Q1, Q3

import produce from 'immer';
let o1 = produce(currentState, draftState => {
  draftState.p.x = 1;
})

Fix Q2

import produce from 'immer';
fn(currentState);
function fn(o) {
  return produce(o, draftState => {
    draftState.p1 = 1;
  })
};

Fix Q4

import produce from 'immer';
let o4 = produce(currentState, draftState => {
  draftState.p.x.push(1);
})

Это очень просто в использовании? Благодаря небольшому тесту мы получили простое представление о Immer. Ниже приводится введение в часто используемый API Immer.

Концептуальная записка

В Immer используется не так много понятий.Здесь задействованные понятия перечислены первыми.Если вы столкнетесь с понятиями, которые не понимаете в процессе чтения этой статьи, вы можете проверить их здесь в любое время.

  • currentState
    Исходное состояние управляемого объекта

  • draftState
    Состояние черновика, сгенерированное из currentState, который является прокси для currentState, и любые изменения, внесенные в draftState, будут регистрироваться и использоваться для создания nextState. Во время этого процесса текущее состояние не будет затронуто

  • nextState
    В конечном состоянии DraftState

  • производить
    функция для генерации nextState или производителя

  • продюсер продюсер
    Генерируется product, используется для создания nextState, каждый раз выполняет одну и ту же операцию

  • машина для производства рецептов
    Функции для управления draftState

Введение в распространенные API

Перед использованием Immer убедитесь, чтоimmerПакеты импортируются в модули

import produce from 'immer'

or

import { produce } from 'immer'

Эти два метода ссылки, продукция точно такие же

produce

Примечания: появляетсяPatchListenerПропустите сначала, будет представлено в последующих главах

Первый способ использования:

грамматика:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState

Пример 1:

let nextState = produce(currentState, (draftState) => {

})

currentState === nextState; // true

Пример 2:

let currentState = {
  a: [],
  p: {
    x: 1
  }
}

let nextState = produce(currentState, (draftState) => {
  draftState.a.push(2);
})

currentState === nextState // false
currentState.a === nextState.a; // false
currentState.p === nextState.p; // true

Видно, что изменения в draftState будут отражены в nextState. Структура, используемая Immer, является общей, а nextState разделяет неизмененную часть с currentState в структуре.Общий эффект показан на рисунке (заимствовано из статьи Immutable в анимации, вторжении и удалении):

Функция автоматической заморозки

Immer также делает очень хитрую внутреннюю вещь, то есть nextState, созданный продуктом, замораживается (замораживается), (Immer внутренне используетObject.freezeметод, замораживает только измененную часть nextState по сравнению с currentState), поэтому при непосредственном изменении nextState будет сообщено об ошибке. Это делает данные nextState действительно неизменяемыми.

Пример:

const currentState = {
  p: {
    x: [2],
  },
};
const nextState = produce(currentState, draftState => {
    draftState.p.x.push(3);
});
console.log(nextState.p.x); // [2, 3]
nextState.p.x = 4;
console.log(nextState.p.x); // [2, 3]
nextState.p.x.push(5); // 报错
2-й способ использования

Используйте характеристики функций высшего порядка для создания производителя производителя.

грамматика:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState

пример:

let producer = produce((draftState) => {
  draftState.x = 2
});
let nextState = producer(currentState);
возвращаемое значение рецепта

Независимо от того, имеет ли рецепт возвращаемое значение, процесс генерации nextState отличается:
Когда рецепт не имеет возвращаемого значения: nextState генерируется в соответствии с draftState;
Когда рецепт имеет возвращаемое значение: nextState генерируется в соответствии с возвращаемым значением функции рецепта;

let nextState = produce(currentState, (draftState) => {
    return {
      x: 5
    }
  }
)
console.log(nextState); // {x: 5}

На этом этапе nextState больше не генерируется draftState, а возвращается значением recipe.

это в рецепте

внутри функции рецептаthisУкажите на draftState , который должен изменитьthisЭффект такой же, как при изменении параметра draftState рецепта.
Примечание. Функция рецепта здесь не может быть стрелочной функцией, если это стрелочная функция,thisне могу указать на draftState

produce(currentState, function(draftState){
  // 此处,this 指向 draftState
  draftState === this; // true
})

Функция патча PATCH

С помощью этой функции можно облегчить детальную отладку и отслеживание кода, узнать о каждой модификации draftState, а также реализовать путешествие во времени.

В Immer объект патча выглядит следующим образом:

interface Patch {
  op: "replace" | "remove" | "add" // 一次更改的动作类型
  path: (string | number)[] // 此属性指从树根到被更改树杈的路径
  value?: any // op为 replace、add 时,才有此属性,表示新的赋值
}

грамматика:

produce(
  currentState, 
  recipe,
  // 通过 patchListener 函数,暴露正向和反向的补丁数组
  patchListener: (patches: Patch[], inversePatches: Patch[]) => void
)

applyPatches(currentState, changes: (patches | inversePatches)[]): nextState

пример:

import produce, { applyPatches } from "immer"

let state = {
  x: 1
}

let replaces = [];
let inverseReplaces = [];

state = produce(
  state,
  draftState => {
    draftState.x = 2;
    draftState.y = 2;
  },
  (patches, inversePatches) => {
    replaces = patches.filter(patch => patch.op === 'replace');
    inverseReplaces = inversePatches.filter(patch => patch.op === 'replace');
  }
)

state = produce(state, draftState => {
  draftState.x = 3;
})
console.log('state1', state); // { x: 3, y: 2 }

state = applyPatches(state, replaces);
console.log('state2', state); // { x: 2, y: 2 }

state = produce(state, draftState => {
  draftState.x = 4;
})
console.log('state3', state); // { x: 4, y: 2 }

state = applyPatches(state, inverseReplaces);
console.log('state4', state); // { x: 1, y: 2 }

state.xЗначение 4-кратного распечатывания результатов:3、2、4、1, осуществляет путешествия во времени, можно распечатать отдельноpatchesа такжеinversePatchesСмотреть,

patchesДанные следующие:

[
  {
    op: "replace",
    path: ["x"],
    value: 2
  },
  {
    op: "add",
    path: ["y"],
    value: 2
  },
]

inversePatchesДанные следующие:

[
  {
    op: "replace",
    path: ["x"],
    value: 1
  },
  {
    op: "remove",
    path: ["y"],
  },
]

видимый,patchListenerВнутри операции с данными записываются и хранятся как записи прямых операций и записи обратных операций для нашего использования.

До сих пор мы представили общие функции и API-интерфейсы Immer.

Далее давайте посмотрим, как использовать Immer для повышения эффективности разработки проектов React и Redux.

Изучение оптимизации реактивных проектов с помощью immer

Сначала определитеstateОбъекты, переменные используются в более поздних примерахstateили посетитьthis.stateКогда нет специальной декларации, это относится к этомуstateобъект

state = {
  members: [
    {
      name: 'ronffy',
      age: 30
    }
  ]
}

бросить спрос

как определено вышеstate, давайте сначала бросим требование, чтобы следующее объяснение могло быть нацелено:
мемберы 1-й член мемберов, возраст увеличен на 1 год

Оптимизируйте метод setState

Пример ошибки

this.state.members[0].age++;

Такие ошибки допускают только некоторые начинающие студенты, основная причина в том, что эта операция настолько удобна, что они забывают правила рабочего состояния.

Вот правильный способ сделать это

Первый метод реализации setState

const { members } = this.state;
this.setState({
  members: [
    {
      ...members[0],
      age: members[0].age + 1,
    },
    ...members.slice(1),
  ]
})

Второй метод реализации setState

this.setState(state => {
  const { members } = state;
  return {
    members: [
      {
        ...members[0],
        age: members[0].age + 1,
      },
      ...members.slice(1)
    ]
  }
})

Вышеуказанные два метода реализацииsetStateВы должны быть знакомы с двумя способами его использования. Далее посмотрим, если для ее решения использовать Immer, какой будет фейерверк?

обновить состояние с помощью immer

this.setState(produce(draftState => {
  draftState.members[0].age++;
}))

Разве это не сразу намного меньше кода и легче читать.

Оптимизировать редукторы

Расширенное использование продуктов иммера

Прежде чем мы начнем формальное исследование, давайте взглянем на продукцию2-й способ использованияРасширенное использование:

пример:

let obj = {};

let producer = produce((draftState, arg) => {
  obj === arg; // true
});
let nextState = producer(currentState, obj);

По сравнению с примером второго употребления продукта определено еще одноobjобъект и передал его как второй параметр метода производителя; вы можете видеть, что второй параметр функции обратного вызова рецепта в продукте такой же, какobjОбъекты указывают на один и тот же блок памяти.
Хорошо, после того, как мы узнаем об этом расширенном использовании продукта, давайте посмотрим, какой эффект он может сыграть в Redux?

Как обычный редуктор решает требования, указанные выше

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_AGE':
      const { members } = state;
      return {
        ...state,
        members: [
          {
            ...members[0],
            age: members[0].age + 1,
          },
          ...members.slice(1),
        ]
      }
    default:
      return state
  }
}

Установите иммер, редуктор, как писать

const reducer = (state, action) => produce(state, draftState => {
  switch (action.type) {
    case 'ADD_AGE':
      draftState.members[0].age++;
  }
})

Как видите, благодаря product объем нашего кода значительно упростился;
Однако нетрудно заметить, что код может быть более элегантным, если использовать характеристики продукта для создания производителя:

const reducer = produce((draftState, action) => {
  switch (action.type) {
    case 'ADD_AGE':
      draftState.members[0].age++;
  }
})

Хорошо, до сих пор был объяснен метод оптимизации редуктора Иммера.

Использование Immer очень гибкое, и есть несколько других расширений API, проведите много исследований, я думаю, вы можете найти больше других замечательных применений Immer!

Справочная документация