Перейти к основному содержимому

Неизменяемые данные

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Redux FAQ: Неизменяемые данные

Какие преимущества даёт неизменяемость?

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

В частности, неизменяемость в веб-приложениях позволяет просто и недорого реализовать сложные методы обнаружения изменений, гарантируя, что ресурсоёмкий процесс обновления DOM происходит только при необходимости (краеугольный камень производительности React по сравнению с другими библиотеками).

Дополнительные материалы

Статьи

Почему Redux требует неизменяемости?

Дополнительные материалы

Документация

Обсуждения

Почему поверхностная проверка равенства в Redux требует неизменяемости?

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

Чем отличаются поверхностная и глубокая проверка равенства?

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

Поверхностная проверка проста и быстра как a === b, а глубокая требует рекурсивного обхода свойств объектов с сравнением значений на каждом шаге.

Именно для повышения производительности Redux использует поверхностную проверку равенства.

Дополнительные материалы

Статьи

Как Redux использует поверхностную проверку равенства?

Redux использует поверхностную проверку равенства в функции combineReducers, чтобы вернуть либо новую изменённую копию корневого объекта состояния, либо текущий корневой объект состояния, если мутаций не было.

Дополнительные материалы

Документация

Как combineReducers использует поверхностную проверку равенства?

Рекомендуемая структура для Redux-хранилища предполагает разделение объекта состояния на несколько "срезов" (slices) или "доменов" по ключам, где каждый срез управляется отдельной функцией-редьюсером.

combineReducers упрощает работу с такой структурой, принимая аргумент reducers в виде хеш-таблицы, содержащей набор пар ключ/значение: каждый ключ соответствует имени среза состояния, а значение — редьюсеру, управляющему этим срезом.

Например, если структура состояния { todos, counter }, вызов combineReducers будет выглядеть так:

combineReducers({ todos: myTodosReducer, counter: myCounterReducer })

где:

  • ключи todos и counter ссылаются на отдельные срезы состояния;

  • значения myTodosReducer и myCounterReducer — функции-редьюсеры, каждый из которых работает с соответствующим срезом состояния.

combineReducers последовательно обрабатывает каждую пару ключ/значение. На каждой итерации он:

  • создаёт ссылку на текущий срез состояния по ключу;

  • вызывает соответствующий редьюсер, передавая ему этот срез;

  • создаёт ссылку на потенциально изменённый срез состояния, возвращённый редьюсером.

В процессе итераций combineReducers формирует новый объект состояния из срезов, возвращённых каждым редьюсером. Этот новый объект может отличаться от текущего состояния или остаться неизменным. Здесь combineReducers использует поверхностную проверку равенства для определения изменений.

Конкретно: на каждом шаге итерации combineReducers выполняет поверхностную проверку равенства текущего среза состояния и среза, возвращённого редьюсером. Если редьюсер возвращает новый объект, проверка завершится неудачей, и combineReducers установит флаг hasChanged в true.

После завершения итераций combineReducers проверяет состояние флага hasChanged. Если он true, возвращается вновь созданный объект состояния. Если false — возвращается текущий объект состояния.

Это важно подчеркнуть: если все редьюсеры возвращают переданный им объект state без изменений, combineReducers вернёт текущий корневой объект состояния, а не обновлённый.

Дополнительные материалы

Документация

Видео

Как React-Redux использует поверхностную проверку равенства?

React-Redux использует поверхностную проверку равенства, чтобы определить, требуется ли повторный рендеринг обёрнутого компонента.

Для этого он предполагает, что обёрнутый компонент является "чистым" (pure), то есть выдаёт одинаковые результаты при одинаковых пропсах и состоянии.

При таком допущении достаточно проверить, изменились ли корневой объект состояния или значения, возвращаемые mapStateToProps. Если изменений нет — повторный рендеринг не требуется.

Он обнаруживает изменения, сохраняя ссылку на корневой объект состояния и ссылки на каждое значение в объекте пропсов, возвращаемых функцией mapStateToProps.

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

Дополнительные материалы

Документация

Статьи

Почему React-Redux проверяет каждое значение в объекте пропсов из mapStateToProp поверхностно?

React-Redux выполняет поверхностную проверку равенства для каждого значения внутри объекта пропсов, а не для самого объекта.

Это происходит потому, что объект пропсов фактически представляет собой хеш с именами свойств и их значениями (или селекторами для получения значений), как в примере:

function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}

export default connect(mapStateToProps)(TodoApp)

Таким образом, поверхностная проверка объекта пропсов при каждом новом вызове mapStateToProps всегда будет проваливаться, так как каждый раз возвращается новый объект.

Поэтому React-Redux хранит отдельные ссылки на каждое значение в возвращаемом объекте пропсов.

Дополнительные материалы

Статьи

Как React-Redux использует поверхностную проверку для определения необходимости перерисовки?

При каждом вызове функции connect из React-Redux выполняется поверхностная проверка между сохранённой ссылкой на корневое состояние и текущим состоянием из хранилища. Если проверка проходит, состояние не обновлялось, поэтому перерисовка компонента или вызов mapStateToProps не требуются.

Если проверка проваливается (состояние обновлено), connect вызывает mapStateToProps для проверки актуальности пропсов компонента.

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

В примере ниже компонент не будет перерисовываться, если state.todos, значение, возвращаемое getVisibleTodos(), и последовательные вызовы connect остаются неизменными.

function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}

export default connect(mapStateToProps)(TodoApp)

В следующем примере компонент будет перерисовываться всегда, так как todos каждый раз создаётся как новый объект, даже если его содержимое не изменилось:

// AVOID - will always cause a re-render
function mapStateToProps(state) {
return {
// todos always references a newly-created object
todos: {
all: state.todos,
visibleTodos: getVisibleTodos(state)
}
}
}

export default connect(mapStateToProps)(TodoApp)

Если поверхностная проверка между новыми и предыдущими (сохранёнными) значениями из mapStateToProps обнаруживает изменения, инициируется перерисовка компонента.

Дополнительные материалы

Статьи

Обсуждения

Почему поверхностная проверка равенства не работает с изменяемыми объектами?

Поверхностная проверка равенства не может обнаружить мутацию объекта, переданного в функцию, если этот объект является изменяемым.

Это происходит потому, что две переменные, ссылающиеся на один и тот же объект, всегда будут равны, независимо от изменения значений внутри объекта. Следовательно, следующее выражение всегда вернет true:

function mutateObj(obj) {
obj.key = 'newValue'
return obj
}

const param = { key: 'originalValue' }
const returnVal = mutateObj(param)

param === returnVal
//> true

Поверхностная проверка param и returnValue просто сравнивает, ссылаются ли обе переменные на один объект, что верно. mutateObj() может возвращать мутировавшую версию obj, но это всё тот же объект, что был передан. Факт изменения его значений внутри mutateObj не влияет на результат поверхностной проверки.

Дополнительные материалы

Статьи

Вызывает ли поверхностная проверка изменяемых объектов проблемы в Redux?

Поверхностная проверка изменяемых объектов не вызывает проблем в Redux, но создает проблемы для библиотек, зависящих от хранилища, таких как React-Redux.

Конкретно: если часть состояния, передаваемая редюсеру через combineReducers, является изменяемым объектом, редюсер может напрямую модифицировать и вернуть его.

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

Соответственно, combineReducers не установит флаг hasChanged, хотя состояние изменилось. Если другие редюсеры не вернут обновлённую часть состояния, флаг hasChanged останется false, и combineReducers вернёт существующий корневой объект состояния.

Хранилище всё равно обновится с новыми значениями корневого состояния, но поскольку сам объект состояния не изменился, библиотеки типа React-Redux не обнаружат изменения и не инициируют перерисовку компонентов.

Дополнительные материалы

Документация

Почему мутация состояния в редюсере мешает React-Redux перерисовывать компонент?

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

Поскольку React-Redux использует поверхностную проверку корневого состояния для определения необходимости перерисовки, он не обнаружит мутацию состояния и не запустит обновление компонента.

Дополнительные материалы

Документация

Почему мутация и возврат персистентного объекта в селекторе mapStateToProps мешает React-Redux обновлять компонент?

Если одно из значений объекта props, возвращаемого из mapStateToProps, является объектом, сохраняющимся между вызовами connect (например, корневым объектом состояния), но при этом мутируется напрямую и возвращается функцией-селектором, React-Redux не сможет обнаружить изменения и не инициирует повторный рендеринг обёрнутого компонента.

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

Например, следующая функция mapStateToProps никогда не вызовет повторный рендеринг:

// State object held in the Redux store
const state = {
user: {
accessCount: 0,
name: 'keith'
}
}

// Selector function
const getUser = state => {
++state.user.accessCount // mutate the state object
return state
}

// mapStateToProps
const mapStateToProps = state => ({
// The object returned from getUser() is always
// the same object, so this wrapped
// component will never re-render, even though it's been
// mutated
userRecord: getUser(state)
})

const a = mapStateToProps(state)
const b = mapStateToProps(state)

a.userRecord === b.userRecord
//> true

Обратите внимание, что в противоположном случае, при использовании иммутабельного объекта, компонент может выполнить рендеринг без необходимости.

Дополнительные материалы

Статьи

Обсуждения

Как иммутабельность позволяет поверхностной проверке обнаруживать мутации объектов?

Если объект иммутабелен, любые изменения должны применяться к его копии, а не к оригиналу.

Эта изменённая копия представляет собой отдельный объект от переданного в функцию. При возврате копии поверхностная проверка определит её как новый объект, что приведёт к неудачному сравнению.

Дополнительные материалы

Статьи

Как иммутабельность в редьюсерах может вызывать избыточный рендеринг компонентов?

Иммутабельный объект нельзя изменить напрямую — вместо этого создаётся и мутируется его копия, оставляя оригинал неизменным.

Это нормально при изменении копии, но если редьюсер возвращает копию без изменений, функция combineReducers из Redux всё равно посчитает, что состояние требует обновления, поскольку возвращённый объект среза состояния полностью отличается от переданного.

Затем combineReducers вернёт этот новый корневой объект состояния в хранилище. Новый объект будет иметь те же значения, что и текущий, но из-за различия в ссылке вызовет обновление хранилища, что в итоге приведёт к избыточному повторному рендерингу всех подключённых компонентов.

Чтобы предотвратить это, всегда возвращайте оригинальный объект среза состояния, переданный в редьюсер, если состояние не изменялось.

Дополнительные материалы

Статьи

Как иммутабельность в mapStateToProps может вызывать избыточный рендеринг?

Некоторые иммутабельные операции, например Array filter, всегда возвращают новый объект, даже если значения не изменились.

Если такая операция используется как функция-селектор в mapStateToProps, поверхностная проверка равенства, которую React-Redux выполняет для каждого значения возвращаемого объекта props, всегда будет проваливаться, поскольку селектор возвращает новый объект при каждом вызове.

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

Например, следующий код всегда вызовет перерендер:

// A JavaScript array's 'filter' method treats the array as immutable,
// and returns a filtered copy of the array.
const getVisibleTodos = todos => todos.filter(t => !t.completed)

const state = {
todos: [
{
text: 'do todo 1',
completed: false
},
{
text: 'do todo 2',
completed: true
}
]
}

const mapStateToProps = state => ({
// getVisibleTodos() always returns a new array, and so the
// 'visibleToDos' prop will always reference a different array,
// causing the wrapped component to re-render, even if the array's
// values haven't changed
visibleToDos: getVisibleTodos(state.todos)
})

const a = mapStateToProps(state)
// Call mapStateToProps(state) again with exactly the same arguments
const b = mapStateToProps(state)

a.visibleToDos
//> { "completed": false, "text": "do todo 1" }

b.visibleToDos
//> { "completed": false, "text": "do todo 1" }

a.visibleToDos === b.visibleToDos
//> false

Обратите внимание, что наоборот — если значения в объекте props ссылаются на изменяемые объекты, ваш компонент может не отрендериться, когда должен.

Дополнительные материалы

Статьи

Какие существуют подходы к работе с неизменяемыми данными? Обязательно ли использовать Immer?

Вам не обязательно использовать Immer с Redux. Обычный JavaScript при правильном использовании вполне способен обеспечить неизменяемость без специализированных библиотек.

Однако гарантировать неизменяемость в JavaScript сложно — легко случайно изменить объект, что вызывает трудноуловимые ошибки. Поэтому использование библиотек для неизменяемых обновлений вроде Immer значительно повышает надёжность приложений и упрощает разработку.

Дополнительные материалы

Обсуждения

Какие проблемы возникают при использовании обычного JavaScript для неизменяемых операций?

JavaScript изначально не проектировался для гарантированно неизменяемых операций. При его использовании для неизменяемых операций в Redux-приложениях нужно учитывать несколько проблем.

Случайное изменение объектов

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

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

Многословный код

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

Низкая производительность

Работа с объектами и массивами JavaScript неизменяемым образом может быть медленной, особенно при увеличении размера дерева состояния.

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

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

Дополнительные материалы

Документация

Статьи