Мутационное состояние напрямую — это плохо. Создание копии требует слишком много ресурсов. Что делать?

avatar
Peeyush Kumar
1 июля 2021 в 20:38
110
2
2

Это небольшая игра, которую я разрабатываю. Снимок экрана

Каждый из этих квадратов представлен объектом.

{
    row: i,
    col: j,
    isWall: false,
    isVisited: false,
    isPath: false,
    parentRow: null,
    parentCol: null,
    distance: Infinity
}

isWall свойство соответствует черным квадратам.

Они хранятся в 2D-массиве в состоянии.

this.state = {
    grid: getGrid()
}

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

getGridCopy = () => this.state.grid.map(row => row.map(square => ({...square})));

turnBlack = (row, col) => {
    const grid = this.getGridCopy();
    grid[row][col].isWall = true;
    this.setState({
        grid
    })
}

(Код урезан, чтобы показать только соответствующие части)

Теперь представьте, что мне нужно анимировать всю сетку от белого до полностью черного по одному квадрату за раз. В сетке сотни квадратов, и я должен изменить каждый из них на черный, а чтобы изменить только один квадрат, я должен скопировать весь 2D-массив объектов. Это оказывается очень ресурсоемким, и я могу явно видеть заикания в анимации. Анимация действительно плавная, когда я меняю состояние напрямую, без копирования.

Что вы предлагаете? У меня нет большого опыта разработки, поэтому любые предложения приветствуются. Это первый проект, который действительно чего-то стоит. Можете ли вы предложить другие способы хранения этих объектов вместо двумерных массивов?

РЕДАКТИРОВАТЬ:

Если есть только один компонент со всей сеткой в ​​его состоянии...

У меня есть компонент Board, который имеет это состояние с массивом grid 2d. Я сопоставил сетку в методе рендеринга компонента платы и визуализировал компонент Node для каждой ячейки. Компонент Node не имеет состояния. Он получает свойства в качестве свойств, применяет соответствующие имена классов к элементам div и отображает их.

//Board component
render() {
    return(
        <div className="node-group">
        {
             grid.map((row, i) => (
                 <div key={i} className="node-row">
                 { row.map((node, j) => <Node {...node} key={j} ></Node> ) }
                 </div>
             ))
        }
        </div>
    )
}

//Node component
render() {
    let className = 'node';
    if (this.props.isWall) className += ' node-wall';
    return( <div className={className} ></div> )
}

Квалифицируется ли это как то, что каждая ячейка является собственным реагирующим компонентом?

Источник
b.stevens.photo
1 июля 2021 в 20:54
0

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

raina77ow
1 июля 2021 в 20:59
0

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

Ответы (2)

avatar
vitoke
2 июля 2021 в 22:11
0

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

Однако, если состояние становится больше или у вас есть какое-то глубоко комбинированное состояние, это может повлиять на производительность, и кодирование становится утомительным. Именно здесь постоянство становится важным. Постоянство гарантирует, что неизменяемые структуры данных сохраняют ссылки на части данных, которые не изменяются, и возвращает копию объекта, ссылающегося на те же «старые» данные, когда это возможно, и новые данные, когда это необходимо. Это делает структуры очень эффективными с точки зрения памяти и производительными.

Есть библиотеки, которые позаботятся о неизменяемости и сохраняемости за вас. Наиболее заметные из них:

  • Immutable.js: библиотека неизменяемых коллекций со встроенной сохраняемостью.

  • Immer: умный способ обновлять объекты, как обычно, без необходимости беспокоиться об изменении оригинала.

Я сам разрабатываю библиотеку неизменяемых постоянных коллекций под названием Rimbu для TypeScript. Он предлагает множество неизменяемых постоянных коллекций, которые вы можете использовать в своем штате, не опасаясь случайного изменения данных.

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

avatar
raina77ow
1 июля 2021 в 20:47
0

Ни одна техника не является плохой или хорошей сама по себе.

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

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

Ключевой вопрос здесь заключается в том, как организованы ваши представления. Если есть только один компонент со всей сеткой в ​​качестве состояния, вам придется заплатить цену — либо при обновлении состояния, либо при попытке обнаружить изменения.

Однако, если каждая ячейка вашей сетки является отдельным представлением (компонентом React), достаточно обновить только этот объект, заменив соответствующий элемент вашего массива новым. Что-то вроде этого:

function handleGridUpdate(changedRow, changedCol, changes) {
  const newGrid = grid.map(
    (row, i) => rows.map(
      (cell, j) => i === changedRow && j === changedCol 
         ? { ...cell, changes }
         : cell
    )
  );
  setGrid(newGrid);
}

... затем вызов этой функции следующим образом:

handleGridUpdate(row, col, { isWall: true } )

В этом примере функция handleGridUpdate принимает координаты измененного элемента сетки, а также изменения, которые необходимо применить. Но хитрость в том, что пока ссылки на списки обновляются (поскольку карта возвращает новую копию массива), их элементы остаются нетронутыми для каждого элемента, который не был изменен. Это экономит много времени — как при копировании, так и при повторном рендеринге компонентов.

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

Для получения дополнительной информации см. эту статью. Несмотря на то, что вам предстоит обработать список, а не сетку, ключевые идеи, продемонстрированные там, можно применить и в вашем случае.

Peeyush Kumar
1 июля 2021 в 21:01
0

Вы имеете в виду вот так? ` const {сетка} = this.state; grid[row][col] = { ...square, isWall: true } ` это не изменяет состояние напрямую?

raina77ow
1 июля 2021 в 21:03
0

Оно делает. Пожалуйста, проверьте фрагмент кода, который я добавил. Вы никогда не должны изменять состояние напрямую; но количество изменений может (и должно) локализоваться в одном элементе сетки.