Redux FAQ: Неизменяемые данные
Содержание
- Какие преимущества обеспечивает неизменяемость?
- Почему неизменяемость требуется в Redux?
-
Почему в Redux используется поверхностная проверка равенства, требующая неизменяемости?
- В чём различие между поверхностной и глубокой проверкой равенства?
- Как Redux использует поверхностную проверку равенства?
- Как
combineReducersиспользует поверхностную проверку равенства? - Как React-Redux использует поверхностную проверку равенства?
- Как React-Redux использует поверхностную проверку равенства, чтобы определить, нужно ли перерисовывать компонент?
- Почему поверхностная проверка равенства не работает с изменяемыми объектами?
- Приводит ли поверхностная проверка равенства с изменяемым объектом к проблемам в Redux?
- Почему мутация состояния в редьюсере препятствует перерисовке обернутого React-Redux компонента?
- Почему мутация и возвращение неизменяемого объекта селектором в
mapStateToPropsпрепятствует перерисовке обернутого React-Redux компонента? - Как неизменяемость позволяет поверхностной проверке обнаруживать мутации объектов?
- Как неизменяемость в редьюсерах может привести к ненужным перерисовкам компонентов?
- Как неизменяемость в mapStateToProps может привести к ненужным перерисовкам компонентов?
- Какие есть способы обработки данных неизменяемым образом? Нужно ли использовать Immutable.JS?
- Какие проблемы возникают при использовании JavaScript для неизменяемых операций?
Какие преимущества обеспечивает неизменяемость?
Неизменяемость может повысить производительность приложения и упростить программирование и отладку, так как данные, которые никогда не меняются, легче анализировать, чем данные, которые могут изменяться произвольным образом в приложении.
В частности, неизменяемость в контексте веб-приложения позволяет реализовать сложные методы обнаружения изменений просто и недорого, гарантируя, что вычисление по обновлению DOM происходит только в том случае, когда это абсолютно необходимо (основа повышения производительности React по сравнению с другими библиотеками).
Дополнительная информация
Статьи
- Введение в Immutable.js и концепции функционального программирования
- Презентация по неизменяемости JavaScript (PDF — см. слайд 12 для преимуществ)
- Immutable.js — Неизменяемые коллекции для JavaScript
- React: Оптимизация производительности
- Архитектура JavaScript-приложений на пути к 2015 году
Почему неизменяемость требуется в Redux?
- Как Redux, так и React-Redux используют поверхностную проверку равенства. В частности:
- Утилита Redux's
combineReducersповерхностно проверяет изменения ссылок, вызванные редьюсерами, которые она вызывает. - Метод React-Redux's
connectгенерирует компоненты, которые поверхностно проверяют изменения ссылок на корневой объект состояния, и значения, возвращаемые функциейmapStateToProps, чтобы увидеть, действительно ли обернутым компонентам нужно перерисовываться. Такая поверхностная проверка требует неизменяемости для правильной работы.
- Утилита Redux's
- Управление неизменяемыми данными в конечном итоге делает обработку данных безопаснее.
- Отладка с "путешествием во времени" требует, чтобы редьюсеры были чистыми функциями без побочных эффектов, чтобы можно было корректно переключаться между различными состояниями.
Дополнительная информация
Документация
Обсуждения
Почему в Redux используется поверхностная проверка равенства, требующая неизменяемости?
Если подключенные компоненты должны обновляться правильно, Redux использует поверхностную проверку равенства, требующую неизменяемости. Чтобы понять почему, необходимо разобраться в различиях между поверхностной и глубокой проверкой равенства в JavaScript.
В чём различие между поверхностной и глубокой проверкой равенства?
Поверхностная проверка равенства (или проверка равенства по ссылке) просто проверяет, ссылаются ли две разные переменные на один и тот же объект; в отличие от этого, глубокая проверка равенства (или проверка равенства по значению) должна проверить каждое значение свойств двух объектов.
Поверхностная проверка равенства поэтому столь же проста (и быстра), как a === b, тогда как глубокая проверка равенства включает рекурсивный обход свойств двух объектов, сравнивая значение каждого свойства на каждом шаге.
Именно для повышения производительности Redux использует поверхностную проверку равенства.
Дополнительная информация
Статьи
Как Redux использует поверхностную проверку равенства?
Redux использует поверхностную проверку равенства в своей функции combineReducers, чтобы вернуть либо новую изменённую копию корневого объекта состояния, либо, если никаких изменений не было сделано, текущий корневой объект состояния.
Дополнительная информация
Документация
Как combineReducers использует поверхностную проверку равенства?
Рекомендуемая структура для магазина Redux — разделение объекта состояния на несколько «кусочков» или «областей» по ключу и предоставление отдельной функции редьюсера для управления каждым отдельным фрагментом данных.
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 вернёт текущий корневой объект состояния, а не обновлённый.
Дополнительная информация
Документация
- API: combineReducers
- Redux FAQ — Как я могу обмениваться состоянием между двумя редьюсерами? Мне нужно использовать
combineReducers?
Видео
Как React-Redux использует поверхностную проверку равенства?
React-Redux использует поверхностную проверку равенства, чтобы определить, нужно ли перерисовывать обернутый компонент.
Для этого предполагается, что обернутый компонент является чистым; то есть, что компонент будет производить одинаковые результаты при одинаковых значениях props и состояния.
Предполагая, что обернутый компонент чистый, достаточно проверить, изменился ли корневой объект состояния или значения, возвращаемые из mapStateToProps. Если нет, перерисовка обернутого компонента не требуется.
Он обнаруживает изменения, сохраняя ссылку на корневой объект состояния и ссылку на каждое значение в объекте props, возвращаемом функцией mapStateToProps.
Затем он выполняет поверхностную проверку равенства по ссылке на корневой объект состояния и переданный ему объект состояния, и серию отдельных поверхностных проверок для каждой ссылки на значения объекта props и тех, что возвращаются при повторном вызове функции mapStateToProps.
Дополнительная информация
Документация
Статьи
- API: Функция connect React-Redux и
mapStateToProps - Устранение неполадок: Мои представления не обновляются, когда что-то меняется вне Redux
Почему React-Redux выполняет поверхностную проверку каждого значения в объекте props, возвращённом из mapStateToProp?
React-Redux выполняет поверхностную проверку равенства каждого значения в объекте props, а не самого объекта props.
Это делается потому, что объект props фактически представляет собой хеш имён свойств и их значений (или функций-селекторов, которые используются для получения или генерации значений), как показано в этом примере:
function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}
export default connect(mapStateToProps)(TodoApp)
Таким образом, поверхностная проверка равенства объекта props, возвращаемого при повторных вызовах mapStateToProps, всегда будет неудачной, так как каждый раз будет возвращаться новый объект.
Поэтому React-Redux сохраняет отдельные ссылки на каждое значение в возвращаемом объекте props.
Дополнительная информация
Статьи
Как React-Redux использует поверхностную проверку равенства для определения необходимости повторного рендеринга компонента?
Каждый раз, когда вызывается функция connect React-Redux, она выполнит поверхностную проверку равенства своей сохранённой ссылки на корневой объект состояния и текущего корневого объекта состояния, переданного ей из хранилища. Если проверка пройдена, корневой объект состояния не был обновлён, и поэтому нет необходимости перерисовывать компонент или даже вызывать mapStateToProps.
Однако, если проверка не пройдена, корневой объект состояния был обновлён, и поэтому connect вызовет mapStateToProps, чтобы проверить, были ли обновлены props обернутого компонента.
Это делается путём выполнения поверхностной проверки равенства каждого значения в объекте по отдельности и будет инициировать повторное рендеринг только в случае неудачи одной из этих проверок.
В примере ниже, если 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, и предыдущими значениями, к которым React-Redux сохранял ссылку, тогда будет инициировано повторное рендеринг компонента.
Дополнительная информация
Статьи
- Практическое руководство по Redux, часть 6: Подключенные списки, формы и производительность
- Антипаттерн производительности React.js Pure Render
- Высокопроизводительные приложения Redux
Обсуждения
Почему поверхностная проверка равенства не работает с изменяемыми объектами?
Поверхностная проверка равенства не может использоваться для определения того, мутирует ли функция объект, переданный ей, если этот объект изменяемый.
Это потому, что две переменные, ссылающиеся на один и тот же объект, всегда будут равны, независимо от того, меняются ли значения объекта или нет, так как обе они ссылаются на один и тот же объект. Таким образом, следующее всегда вернёт 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 вернуть существующий корневой объект состояния.
Хранилище всё равно будет обновлено с новыми значениями корневого состояния, но поскольку сам корневой объект состояния остаётся тем же объектом, библиотеки, связанные с Redux, такие как React-Redux, не будут знать об изменении состояния и не вызовут повторного рендеринга обернутого компонента.
Дополнительная информация
Документация
- Рецепты: Шаблоны неизменяемых обновлений
- Устранение неполадок: Никогда не изменяйте аргументы редьюсера
Почему мутация состояния редьюсером препятствует повторному рендерингу обернутого компонента React-Redux?
Если редьюсер 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 затем вернёт этот новый корневой объект состояния в хранилище. Новый объект будет иметь те же значения, что и текущий корневой объект состояния, но поскольку это другой объект, это приведёт к обновлению хранилища, что в конечном итоге приведёт к ненужному перерисовке всех подключённых компонентов.
Чтобы этого избежать, вы всегда должны возвращать объект среза состояния, который передаётся в редьюсер, если редьюсер не изменяет состояние.
Дополнительная информация
Статьи
- Антипаттерн производительности React.js pure render
- Создание эффективных пользовательских интерфейсов с React и Redux
Как неизменяемость в 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 ссылаются на изменяемые объекты, ваш компонент может не перерисоваться, когда это необходимо.
Дополнительная информация
Статьи
- Антипаттерны производительности чистого рендеринга React.js
- Создание эффективных пользовательских интерфейсов с React и Redux
- ImmutableJS: стоит ли цена?
Какие подходы существуют для обработки данных неизменяемым образом? Нужно ли использовать Immutable.JS?
Вам не нужно использовать Immutable.JS с Redux. Обычный JavaScript, если он написан правильно, отлично справляется с обеспечением неизменяемости без использования библиотеки, ориентированной на неизменяемость.
Однако гарантирование неизменяемости с помощью JavaScript затруднительно, и легко случайно изменить объект, вызвав ошибки в вашем приложении, которые очень сложно обнаружить. По этой причине использование библиотеки утилиты для неизменяемых обновлений, такой как Immutable.JS, может значительно повысить надёжность вашего приложения и упростить разработку.
Дополнительная информация
Обсуждения
- #1185: Вопрос: Нужно ли использовать неизменяемые структуры данных?
- Введение в Immutable.js и концепции функционального программирования
В чём проблемы использования обычного JavaScript для неизменяемых операций?
JavaScript никогда не был разработан для предоставления гарантированных неизменяемых операций. Соответственно, есть несколько проблем, о которых вам нужно знать, если вы решите использовать его для неизменяемых операций в вашем приложении Redux.
Случайное изменение объекта
В JavaScript вы можете довольно легко случайно изменить объект (например, дерево состояния Redux), не осознавая этого. Например, обновление глубоко вложенных свойств, создание новой ссылки на объект вместо нового объекта или выполнение поверхностной копии вместо глубокой копии могут привести к непреднамеренным изменениям объектов и могут затруднить работу даже самым опытным разработчикам JavaScript.
Чтобы избежать этих проблем, убедитесь, что вы следуете рекомендуемым паттернам обновления неизменяемых данных для ES6.
Многословный код
Обновление сложных вложенных деревьев состояния может привести к многословному коду, который утомителен для написания и сложен для отладки.
Низкая производительность
Работа с объектами и массивами JavaScript неизменяемым образом может быть медленной, особенно по мере увеличения размера вашего дерева состояния.
Помните, чтобы изменить неизменяемый объект, необходимо изменить копию, а копирование больших объектов может быть медленным, так как каждое свойство должно быть скопировано.
В отличие от этого, такие неизменяемые библиотеки, как Immutable.JS, могут использовать сложные методы оптимизации, такие как совместное использование структуры, которые эффективно возвращают новый объект, использующий большую часть существующего объекта, копируемого из него.
При копировании очень больших объектов обычный JavaScript может быть более чем в 100 раз медленнее, чем оптимизированная неизменяемая библиотека.
Дополнительная информация
Документация
Статьи
- Immutable.js, постоянные структуры данных и совместное использование структуры
- Глубокий анализ структур данных Clojure
- Введение в Immutable.js и концепции функционального программирования
- JavaScript и неизменяемость
- Неизменяемый Javascript с использованием ES6 и далее
- Плюсы и минусы использования неизменяемости с React.js - React Kung Fu
© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/faq/ImmutableData.html