Redux 常见问题解答:不可变数据
目录
- 不可变性的好处是什么?
- 为什么 Redux 需要不可变性?
- 为什么 Redux 使用的浅层相等性检查需要不可变性?
- 浅层和深层相等性检查有什么区别?
- Redux 如何使用浅层相等性检查?
combineReducers
如何使用浅层相等性检查?- React-Redux 如何使用浅层相等性检查?
- React-Redux 如何使用浅层相等性检查来确定组件是否需要重新渲染?
- 为什么浅层相等性检查不适用于可变对象?
- 使用可变对象进行浅层相等性检查会导致 Redux 出现问题吗?
- 为什么 reducer 改变状态会阻止 React-Redux 重新渲染包装的组件?
- 为什么选择器改变并返回持久对象到
mapStateToProps
会阻止 React-Redux 重新渲染包装的组件? - 不可变性如何使浅层检查能够检测到对象更改?
- reducer 中的不可变性如何导致组件不必要地渲染?
- 在 mapStateToProps 中,不可变性如何导致组件不必要地重新渲染?
- 有哪些方法可以处理数据不可变性?我必须使用 Immer 吗?
- 使用 JavaScript 进行不可变操作有哪些问题?
不可变性的好处是什么?
不可变性可以提高应用程序的性能,并简化编程和调试,因为永远不会改变的数据比可以在整个应用程序中任意更改的数据更容易推理。
特别是,在 Web 应用程序的上下文中,不可变性使复杂的更改检测技术能够简单且廉价地实现,确保仅在绝对必要时才执行更新 DOM 的计算密集型过程(这是 React 性能优于其他库的关键所在)。
更多信息
文章
为什么 Redux 需要不可变性?
- Redux 和 React-Redux 都使用 浅层相等性检查。特别是
- Redux 的
combineReducers
实用程序 浅层检查由其调用的 reducer 引起的引用更改。 - React-Redux 的
connect
方法生成 浅层检查根状态的引用更改 和mapStateToProps
函数的返回值,以查看包装的组件是否确实需要重新渲染。这种 浅层检查需要不可变性 才能正常工作。
- Redux 的
- 不可变的数据管理最终使数据处理更安全。
- 时间旅行调试要求 reducer 是没有副作用的纯函数,以便您可以正确地在不同状态之间跳转。
更多信息
文档
讨论
为什么 Redux 使用浅比较需要不可变性?
如果任何连接的组件要正确更新,Redux 使用的浅比较需要不可变性。要了解原因,我们需要理解 JavaScript 中浅比较和深比较的区别。
浅比较和深比较有什么区别?
浅比较(或引用相等)只是检查两个不同的变量是否引用同一个对象;相反,深比较(或值相等)必须检查两个对象属性的每个值。
因此,浅比较与 a === b
一样简单(和快速),而深比较涉及对两个对象的属性进行递归遍历,在每一步比较每个属性的值。
正是为了提高性能,Redux 使用了浅比较。
更多信息
文章
Redux 如何使用浅比较?
Redux 在其 combineReducers
函数中使用浅比较,以返回根状态对象的新的变异副本,或者,如果未进行任何变异,则返回当前根状态对象。
更多信息
文档
combineReducers
如何使用浅比较?
Redux 存储的建议结构是将状态对象按键拆分为多个“切片”或“域”,并提供单独的 reducer 函数来管理每个单独的数据切片。
combineReducers
通过接受一个 reducers
参数来简化这种结构的使用,该参数定义为一个哈希表,包含一组键值对,其中每个键都是状态切片的名称,对应的值是将对其进行操作的 reducer 函数。
例如,如果您的状态形状是 { todos, counter }
,则对 combineReducers
的调用将是
combineReducers({ todos: myTodosReducer, counter: myCounterReducer })
其中
- 键
todos
和counter
分别引用一个单独的状态切片; myTodosReducer
和myCounterReducer
是 reducer 函数,每个函数都作用于由各自键标识的 state 切片。
combineReducers
遍历每个键值对。对于每次迭代,它
- 创建一个对由每个键引用的当前 state 切片的引用;
- 调用相应的 reducer 并将切片传递给它;
- 创建一个对 reducer 返回的可能已变异的 state 切片的引用。
在继续迭代时,combineReducers
将使用从每个 reducer 返回的 state 切片构建一个新的 state 对象。这个新的 state 对象可能与当前 state 对象不同,也可能相同。combineReducers
在这里使用浅比较来确定 state 是否已更改。
具体来说,在每次迭代阶段,combineReducers
对当前 state 切片和从 reducer 返回的 state 切片进行浅比较。如果 reducer 返回一个新对象,则浅比较将失败,combineReducers
将将 hasChanged
标志设置为 true。
迭代完成后,combineReducers
将检查 hasChanged
标志的状态。如果为 true,则将返回新构建的 state 对象。如果为 false,则返回当前 state 对象。
值得强调的是:如果所有 reducer 都返回传递给它们的相同 state
对象,则 combineReducers
将返回当前根 state 对象,而不是新更新的 state 对象。
更多信息
文档
视频
React-Redux 如何使用浅比较?
React-Redux 使用浅比较来确定它包装的组件是否需要重新渲染。
为了实现这一点,它假设包装的组件是纯净的;也就是说,该组件在给定相同 props 和 state 时会产生相同的结果。
通过假设包装的组件是纯净的,它只需要检查根状态对象或从 mapStateToProps
返回的值是否发生了变化。如果它们没有发生变化,则包装的组件不需要重新渲染。
它通过保留对根状态对象的引用以及对从 mapStateToProps
函数返回的 props 对象中每个值的引用来检测更改。
然后,它对对根状态对象的引用和传递给它的状态对象进行浅层相等性检查,并对对 props 对象值的每个引用以及从再次运行 mapStateToProps
函数返回的值进行一系列单独的浅层检查。
更多信息
文档
文章
为什么 React-Redux 对从 mapStateToProp
返回的 props 对象中的每个值进行浅层检查?
React-Redux 对 props 对象中的每个值执行浅层相等性检查,而不是对 props 对象本身进行检查。
它之所以这样做是因为 props 对象实际上是 prop 名称及其值的哈希表(或用于检索或生成值的 selector 函数),例如在这个示例中
function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}
export default connect(mapStateToProps)(TodoApp)
因此,对从重复调用 mapStateToProps
返回的 props 对象进行浅层相等性检查将始终失败,因为每次都会返回一个新对象。
因此,React-Redux 对返回的 props 对象中的每个值维护单独的引用。
更多信息
文章
React-Redux 如何使用浅比较来判断组件是否需要重新渲染?
每次调用 React-Redux 的 connect
函数时,它都会对存储的根状态对象引用和从 store 传递给它的当前根状态对象进行浅比较。如果检查通过,则根状态对象没有更新,因此不需要重新渲染组件,甚至不需要调用 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 保留引用的先前值之间的浅比较失败,则将触发组件的重新渲染。
更多信息
文章
讨论
为什么浅比较不适用于可变对象?
浅比较不能用于检测函数是否会修改传递给它的可变对象。
这是因为引用同一个对象的两个变量始终相等,无论对象的值是否改变,因为它们都引用同一个对象。因此,以下代码将始终返回 true
function mutateObj(obj) {
obj.key = 'newValue'
return obj
}
const param = { key: 'originalValue' }
const returnVal = mutateObj(param)
param === returnVal
//> true
对param
和returnValue
的浅层检查只是检查两个变量是否引用同一个对象,而它们确实引用了同一个对象。mutateObj()
可能会返回obj
的变异版本,但它仍然与传入的obj
是同一个对象。在mutateObj
中更改其值对浅层检查没有任何影响。
更多信息
文章
使用可变对象进行浅层相等性检查会导致 Redux 出现问题吗?
使用可变对象进行浅层相等性检查不会导致 Redux 出现问题,但它会导致依赖于存储的库(如 React-Redux)出现问题.
具体来说,如果传递给 combineReducers
的状态切片是一个可变对象,则 reducer 可以直接修改它并返回它。
如果这样做,combineReducers
执行的浅层相等性检查将始终通过,因为 reducer 返回的状态切片的值可能已被修改,但对象本身没有被修改 - 它仍然是传递给 reducer 的同一个对象。
因此,combineReducers
不会设置其 hasChanged
标志,即使状态已更改。如果其他 reducer 都不返回新的更新状态切片,hasChanged
标志将保持为 false,导致 combineReducers
返回现有的根状态对象。
存储仍然会使用根状态的新值进行更新,但由于根状态对象本身仍然是同一个对象,绑定到 Redux 的库(如 React-Redux)将不会意识到状态的变异,因此不会触发包装组件的重新渲染。
更多信息
文档
为什么 reducer 改变状态会导致 React-Redux 无法重新渲染包装的组件?
如果 Redux reducer 直接改变并返回传入它的状态对象,根状态对象的数值会改变,但对象本身不会改变。
因为 React-Redux 对根状态对象进行浅层检查,以确定其包装的组件是否需要重新渲染,它无法检测到状态改变,因此不会触发重新渲染。
更多信息
文档
为什么 selector 改变并返回一个持久对象到 mapStateToProps
会导致 React-Redux 无法重新渲染包装的组件?
如果 mapStateToProps
返回的 props 对象中的某个值是一个在调用 connect
时持续存在的对象(例如,可能是根状态对象),但它被 selector 函数直接改变并返回,React-Redux 将无法检测到改变,因此不会触发包装组件的重新渲染。
正如我们所见,selector 函数返回的可变对象中的数值可能发生了改变,但对象本身没有改变,而浅层相等性检查只比较对象本身,而不是它们的数值。
例如,以下 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
请注意,相反,如果使用不可变对象,组件可能会在不应该重新渲染时重新渲染。
更多信息
文章
讨论
不可变性如何使浅层检查能够检测到对象改变?
如果一个对象是不可变的,那么在函数中需要对它进行的任何更改都必须对对象的副本进行。
这个被改变的副本是与传入函数的对象分离的,因此当它被返回时,浅层检查会识别它是一个与传入对象不同的对象,因此会失败。
更多信息
文章
reducer 中的不可变性如何会导致组件不必要地渲染?
您不能修改不可变对象;相反,您必须修改它的副本,保持原始对象不变。
当您修改副本时,这完全没问题,但在 reducer 的上下文中,如果您返回一个未被修改的副本,Redux 的 combineReducers
函数仍然会认为状态需要更新,因为您返回的是与传入的状态切片对象完全不同的对象。
combineReducers
然后会将这个新的根状态对象返回到存储中。新对象将与当前根状态对象具有相同的值,但由于它是一个不同的对象,它将导致存储更新,最终导致所有连接的组件不必要地重新渲染。
为了防止这种情况发生,您必须始终返回传递到 reducer 的状态切片对象,如果 reducer 没有修改状态。
更多信息
文章
mapStateToProps
中的不可变性如何导致组件不必要地渲染?
某些不可变操作,例如数组过滤,将始终返回一个新对象,即使值本身没有改变。
如果这样的操作用作 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 吗?
您不需要在 Redux 中使用 Immer。如果编写正确,纯 JavaScript 完全能够提供不可变性,而无需使用不可变性库。
但是,使用 JavaScript 保证不可变性很困难,并且很容易意外地修改对象,从而导致应用程序中的错误,这些错误非常难以定位。出于这个原因,使用像 Immer 这样的不可变更新实用程序库可以显着提高应用程序的可靠性,并使应用程序的开发变得更加容易。
更多信息
讨论
使用纯 JavaScript 进行不可变操作有哪些问题?
JavaScript 从未设计为提供保证不可变的操作。因此,如果您选择在 Redux 应用程序中使用 JavaScript 进行不可变操作,则需要了解以下几个问题。
意外对象变异
使用 JavaScript,您可能会在不知不觉中意外地变异一个对象(例如 Redux 状态树)。例如,更新深层嵌套的属性,创建对对象的新的引用而不是新的对象,或执行浅拷贝而不是深拷贝,都可能导致意外的对象变异,甚至可能让最有经验的 JavaScript 程序员也措手不及。
为了避免这些问题,请确保您遵循推荐的不可变更新模式。
冗长的代码
更新复杂的嵌套状态树会导致冗长的代码,这些代码难以编写且难以调试。
性能低下
以不可变的方式操作 JavaScript 对象和数组可能会很慢,尤其是在您的状态树越来越大的情况下。
请记住,要更改不可变对象,您必须变异它的副本,而复制大型对象可能会很慢,因为每个属性都必须被复制。
相比之下,像 Immer 这样的不可变库可以采用结构共享,这有效地返回一个新的对象,该对象重用了被复制的现有对象的很大一部分。
更多信息
文档
文章