Неизменяемые данные в React — Immer

React.js
Неизменяемые данные в React — Immer

Что такое Иммер?

Immer — это библиотека Javascript для неизменяемых данных, которая упрощает работу с неизменяемыми данными.

Что такое неизменяемые данные?

Концепция неизменяемых данных пришла из функционального программирования. В функциональном программировании инициализированная «переменная» не может быть изменена, и для каждого изменения должна создаваться новая «переменная».

JavaScript не достигнут на немобойных данных уровня языка, нуждается в помощи сторонних библиотек для достижения. Иммер один из них понял (есть аналогичные Immutable.js).

Зачем использовать неизменяемые данные?

В разделе оптимизации производительности React я долго представлял его.shouldComponentUpdate, из этого также получаются неизменяемые данные. Использование неизменяемых данных может решить проблемы, связанные с оптимизацией производительности, поэтому сосредоточьтесь на этой части фона.

Оптимизация производительности в React

Избегайте примирения

когда компонентpropsилиstateизменения, React сравнит только что возвращенный элемент с ранее визуализированным элементом, чтобы определить, необходимо ли обновлять настоящий DOM. Когда они не идентичны, React обновляет этот DOM. Хотя React уже гарантирует, что неизмененные элементы не будут обновлены, даже если React обновляет только измененные узлы DOM, повторный рендеринг все равно занимает некоторое время. В большинстве случаев это не проблема, но если это заметно медленно, вы можете переопределить методы жизненного цикла, переопределивshouldComponentUpdateчтобы ускорить. Этот метод будет вызываться перед повторным рендерингом. Его реализация по умолчанию всегда возвращаетtrue, пусть React выполнит обновление:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

Если вы знаете, когда ваш компонент не нуждается в обновлении, вы можетеshouldComponentUpdateвернуться вfalseчтобы пропустить весь процесс рендеринга. который включает в себя компонентrenderвызова и последующих операций.

СЛЕДУЕТОбновить компонент

Это поддерево компонентов. В каждом узлеSCUпредставлятьshouldComponentUpdateвозвращаемое значение, аvDOMEqПредставляет, совпадают ли возвращаемые элементы React. Наконец, цвет кружка указывает на необходимость согласования компонента.

should-component-update
Узел C2shouldComponentUpdateвернулсяfalse, поэтому React не будет вызывать C2render, и поэтому C4 и C5shouldComponentUpdateне будет называться.

Для C1 и C3,shouldComponentUpdateвернулсяtrue, поэтому React должен продолжать запрашивать дочерние узлы вниз. Здесь С6shouldComponentUpdateвернулсяtrue, и из-заrenderВозвращаемый элемент отличается от того, что было до того, как React обновил DOM.

Последний интересный пример — C8. React должен вызвать этот компонентrender, но поскольку он возвращает тот же элемент React, что и раньше, нет необходимости обновлять DOM.

Очевидно, вы видите, что React изменяет только C6 DOM. Для C8 рендеринг реального DOM пропускается за счет сравнения визуализированного элемента React. А для детей С2 и С7, т.к.shouldComponentUpdateсделатьrenderи не был вызван. Так что контрастный элемент им тоже не нужен.

Пример

В последнем разделе есть интересный пример C8, который совсем не изменился, но React его согласовал. Мы можем полностью избежать таких проблем с помощью условного суждения, избежать согласования и оптимизировать производительность.

Если ваш компонент работает только тогда, когдаprops.colorилиstate.countЗначение изменяется только тогда, когда его необходимо обновить, вы можете использоватьshouldComponentUpdateпроверить:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

В этом кодеshouldComponentUpdateтолько провереноprops.colorилиstate.countменять ли. Если эти значения не изменились, то компонент обновляться не будет. Если ваши компоненты немного сложнее, вы можете использовать такой шаблон, как «поверхностное сравнение», чтобы проверитьpropsа такжеstateВсе поля в компоненте для определения необходимости обновления компонента. React уже предоставляет хороший помощник, который поможет вам реализовать этот распространенный шаблон — вы просто наследуетеReact.PureComponentВот и все (функциональные компоненты используютReact.memo). Таким образом, этот код можно изменить на более краткую форму:

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

ноReact.PureComponentДелаются только поверхностные сравнения, поэтому, когдаpropsилиstateЕсли оно в какой-то степени изменчиво, поверхностное сравнение будет пропущено, и вы не сможете его использовать. Например, использование массива или объекта: (следующий код неверен)

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

wordsиспользование массиваpushметод добавляет элемент, ноstateдержалwordsцитаты не изменились.pushСами данные изменяются напрямую, а новые данные не генерируются, и это изменение не может быть воспринято поверхностным сравнением. React производит неправильное поведение и не выполняет повторноrender. Еще одна проблема вводится для оптимизации производительности.

Сила неизменяемых данных

Самый простой способ избежать этой проблемы — не менятьpropsилиstateценность . Например, вышеhandleClickможно использовать методconcatПереписать:

handleClick() {
  this.setState(state => ({
    words: state.words.concat(['marklar'])
  }));
}

Или используйте оператор расширения массива ES6:

handleClick() {
  this.setState(state => ({
    words: [...state.words, 'marklar'],
  }));
};

Но при работе с глубоко вложенными объектами их обновление неизменяемым (неизменяемым) способом вызывает недоумение. Например, вы можете написать такой код:

handleClick() {
  this.setState(state => ({
    objA: {
      ...state.objA,
      objB: {
        ...state.objA.objB,
        objC: {
          ...state.objA.objB.objC,
          stringA: 'string',
        }
      },
    },
  }));
};

Нам нужна более дружественная библиотека, помогающая нам интуитивно использовать неизменяемые (неизменяемые) данные.

Почему бы не использовать глубокое копирование/сравнение?

Глубокая копия позволит всем компонентам получать новые данные, позволяяshouldComponentUpdateневерный. Глубокие сравнения каждый раз сравнивают все значения, а когда иерархия данных глубокая и меняется только одно значение, эти сравнения — пустая трата производительности.

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

Почему Иммер?

immutable.js

  • Я поддерживаю набор структур данных, типы данных Javascript иimmutable.jsТипы должны быть преобразованы друг в друга, что является навязчивым для данных.
  • Размер библиотеки относительно велик (63 КБ), что не подходит для мобильного терминала с небольшим размером пакета.
  • API чрезвычайно богат, а стоимость обучения высока.
  • Совместимость очень хорошая и поддерживает старые версии IE.

immer

  • Реализовано с помощью Proxy, плохая совместимость.
  • Размер небольшой (12кб), и мобила удобна.
  • API лаконичен, использует собственные типы данных Javascript и почти не требует затрат на понимание.

Сравнивая преимущества и недостатки, недостатки совместимости immer совершенно незначительны в нашей среде. По-прежнему намного проще использовать библиотеку, которая не обременяет другие концепции.

Обзор погружения

Иммер на основеcopy-on-writeмеханизм.

Основная идея Immer заключается в том, что все изменения применяются к временнымdraftState,этоcurrentStateагент. После внесения всех изменений Immer будет сгенерирован на основе изменений в статусе черновика.nextState. Это означает, что с данными можно взаимодействовать, просто изменяя их, сохраняя при этом все преимущества неизменяемых данных.

immer

Этот раздел вращается вокругproduceПредставлен этот основной API. Immer также предоставляет некоторые вспомогательные API, см. подробностиофициальная документация.

Основной API: производить

Синтаксис 1:

produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState

Синтаксис 2:

produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState

использовать продукты

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

В приведенном выше примереdraftStateизменения отразятся наnextState, и не будет изменятьbaseState. Хотя структура, используемая immer, является общей,nextStateструктурно сcurrentStateПоделитесь неизмененными частями.

// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)

// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)

// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])

Учебная программа

ДатьproduceЛечение будет выполнено, когда в функцию будет передан первый аргумент. Он возвращает функцию, которая принимает аргументы, которые передаются вproduceФункция, которую нужно получить при кураторстве. Пример:

// mapper will be of signature (state, index) => state
const mapper = produce((draft, index) => {
    draft.index = index
})

// example usage
console.dir([{}, {}, {}].map(mapper))
// [{index: 0}, {index: 1}, {index: 2}])

Этот механизм может быть упрощен для хорошего использованияreducer:

import produce from "immer"

const byId = produce((draft, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            action.products.forEach(product => {
                draft[product.id] = product
            })
            return
    }
})

возвращаемое значение рецепта

как правило,recipeНе возвращает ничего, что не нужно отображать,draftStateбудет автоматически отражено как возвращаемое значение дляnextState. Вы также можете вернуть произвольные данные какnextStateПри условии, чтоdraftStateНе изменен.

const userReducer = produce((draft, action) => {
    switch (action.type) {
        case "renameUser":
            // OK: we modify the current state
            draft.users[action.payload.id].name = action.payload.name
            return draft // same as just 'return'
        case "loadUsers":
            // OK: we return an entirely new state
            return action.payload
        case "adduser-1":
            // NOT OK: This doesn't do change the draft nor return a new state!
            // It doesn't modify the draft (it just redeclares it)
            // In fact, this just doesn't do anything at all
            draft = {users: [...draft.users, action.payload]}
            return
        case "adduser-2":
            // NOT OK: modifying draft *and* returning a new state
            draft.userCount += 1
            return {users: [...draft.users, action.payload]}
        case "adduser-3":
            // OK: returning a new state. But, unnecessary complex and expensive
            return {
                userCount: draft.userCount + 1,
                users: [...draft.users, action.payload]
            }
        case "adduser-4":
            // OK: the immer way
            draft.userCount += 1
            draft.users.push(action.payload)
            return
    }
})

Очевидно, что этот путь не может вернутьсяundefined.

produce({}, draft => {
    // don't do anything
})
produce({}, draft => {
    // Try to return undefined from the producer
    return undefined
})

Потому что в Javascript не возвращайте никакого значения и возвращайтеundefinedто же самое, возвращаемое значение функции равноundefined. Если вы хотите, чтобы immer знал, что вы хотите вернутьсяundefinedКак сделать? Используйте встроенные переменные immernothing:

import produce, {nothing} from "immer"

const state = {
    hello: "world"
}

produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}

produce(state, draft => nothing)
// Produces a new state, 'undefined'

Автоматическая заморозка

Immer автоматически заморозится для использованияproduceМодифицированное дерево состояний, которое предотвращает изменение дерева состояний вне функции изменения. Эта функция влияет на производительность, поэтому ее необходимо отключить в рабочей среде. можно использоватьsetAutoFreeze(true / false)Открыть или закрыть. Рекомендуется открывать в среде разработки, чтобы избежать непредсказуемых изменений дерева состояний.

использовать immer в setState

Обновления глубокого состояния с помощью immer просты:

/**
 * Classic React.setState with a deep merge
 */
onBirthDayClick1 = () => {
    this.setState(prevState => ({
        user: {
            ...prevState.user,
            age: prevState.user.age + 1
        }
    }))
}

/**
 * ...But, since setState accepts functions,
 * we can just create a curried producer and further simplify!
 */
onBirthDayClick2 = () => {
    this.setState(
        produce(draft => {
            draft.user.age += 1
        })
    )
}

на основеproduceОбеспечивает характеристику курирования, непосредственно конвертируяproduceКаррированное возвращаемое значение передаетсяthis.setStateВот и все. существуетrecipeВнутренне сделайте изменения состояния, которые вы хотите сделать. Интуитивный, не вводящий новых понятий.

Использование иммера в качестве крючка

Immer также предоставляет библиотеку хуков React.use-immerИспользуется для зацепления иммерса.

useImmer

useImmerа такжеuseStateочень похожи. Он принимает начальное состояние и возвращает массив. Первое значение массива — это текущее состояние, а второе значение — функция обновления состояния. функция обновления состояния иproduceсерединаrecipeРаботает так же.

import React from "react";
import { useImmer } from "use-immer";


function App() {
  const [person, updatePerson] = useImmer({
    name: "Michel",
    age: 33
  });

  function updateName(name) {
    updatePerson(draft => {
      draft.name = name;
    });
  }

  function becomeOlder() {
    updatePerson(draft => {
      draft.age++;
    });
  }

  return (
    <div className="App">
      <h1>
        Hello {person.name} ({person.age})
      </h1>
      <input
        onChange={e => {
          updateName(e.target.value);
        }}
        value={person.name}
      />
      <br />
      <button onClick={becomeOlder}>Older</button>
    </div>
  );
}

Очевидно, что для данного примера роль иммера представить невозможно :). Просто пример, чтобы показать использование.

useImmerReducer

правильноuseReducerУпаковка:

import React from "react";
import { useImmerReducer } from "use-immer";

const initialState = { count: 0 };

function reducer(draft, action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return void draft.count++;
    case "decrement":
      return void draft.count--;
  }
}

function Counter() {
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

Справочная статья