结合 Immutable.JS 使用 Redux
目录
- 为什么应该使用 Immutable.JS 等不可变的库?
- 为什么应该选择 Immutable.JS 作为不可变的库?
- 使用 Immutable.JS 有什么问题?
- Immutable.JS 是否值得使用?
- 在 Redux 中使用 Immutable.JS 有哪些最佳实践?
为什么应该使用 Immutable.JS 等不可变的库?
Immutable.JS 不可变的库被设计旨在解决 JavaScript 中固有的不可变(Immutability)问题,为应用程序提供不可变带来的所有好处。
你是选择使用这样的库,还是坚持使用简单的 JavaScript,完全取决于你对向应用程序中添加另一个依赖是满意程度,或者取决于你是否确信使用它可以避免 JavaScript 处理不可变的方法中固有的缺陷。
无论你做何选择,请确保你熟悉关于不可变(immutability)和突变(mutation)以及副作用的概念。尤其要确保你深入了解 JavaScript 在更新和复制值时所做的操作,以防止意外的突变(mutation)导致应用程序性能的降低,甚至完全破坏应用程序的性能。
更多信息
文档
- 技巧:关于不可变(immutability)和突变(mutation)以及副作用
文章
- Immutable.js 和函数式编程概念介绍
- React.js 使用不可变的优点和缺点
为什么应该选择 Immutable.JS 作为不可变的库?
Immutable.JS 旨在以一种高性能的方式提供不可变,以克服 JavaScript 不可变的局限性。其主要优点包括:
保证不可变
封装在 Immutable.JS 对象中的数据永远不会发生变换(mutate)。总是会返回一个新的拷贝对象。这与 JavaScript 相反,其中一些操作不会改变数据(例如,一些数组方法,包括 map,filter,concat,forEach 等),但有一些操作会改变数据(Array 的 pop,push,splice 等)。
丰富的 API
Immutable.JS 提供了一组丰富的不可变对象来封装数据(例如,Maps,Lists,Sets,Records 等),以及一系列操作它们的方法,包括 sort,filter,数据分组,reverse,flatten 以及创建子集等方法。
高性能
Immutable.JS 在实现过程中针对性能优化做了很多工作。这是非常关键的功能,因为使用不可变的数据结构可能需要进行大量昂贵的复制。尤其是对大型复杂数据集(如嵌套的 Redux state tree(状态树))进行不可变操作时,中间可能会产生很多拷贝对象,当浏览器的垃圾回收器清理对象时,这些拷贝对象会消耗内存并降低性能。
Immutable.JS 内部通过巧妙共享数据结构避免了这种情况,最大限度地减少了拷贝数据的情况。它还能执行复杂的操作链,而不会产生不必要的(且昂贵的)中间数据克隆,这些数据很快就会被丢弃。
你决不会看到这些,当然 - 你给 Immutable.JS 对象的数据永远不会发生变化。但是,它从 Immutable.JS 中生成的 intermediate 数据,可以通过链式调用序列中的数据进行自由的变换。因此,你可以拥有不可变数据结构的所有优势,并且不会产生任何潜在的(或很少)性能问题。
更多信息
文章
- Immutable.js,持续化数据结构与结构共享
- PDF: JavaScript Immutability - 不要更改
库
- Immutable.js
使用 Immutable.JS 有什么问题?
尽管功能强大,但 Immutable.JS 还是需要谨慎使用,因为它存在它自己的问题。注意,所有这些问题都可以通过谨慎编码轻松解决。
交互操作困难
JavaScript 没有提供不可变的数据结构。因此,要保证 Immutable.JS 其不可变,你的数据就必须封装在 Immutable.JS 对象(例如:Map
或 List
等)中。一旦使用这种方式包裹数据,这些数据就很难与其他普通的 JavaScript 对象进行交互操作。
例如,你将不再能够通过标准 JavaScript 中的点语法或中括号引用对象的属性。相反,你必须通过 Immutable.JS 提供的 get()
或 getIn()
方法来引用它们,这些方法使用了一种笨拙的语法,通过一个字符串字符串数组访问属性,每个字符串代表一个属性的 key。
例如,你将使用 myImmutableMap.getIn(['prop1', 'prop2', 'prop3'])
替代 myObj.prop1.prop2.prop3
。
这不仅使得与你自己的代码进行交互操作变得尴尬,而且还与其他库(如 lodash 或 ramda)的交互也会很尴尬,这些库都需要普通的 JavaScript 对象。
注意,Immutable.JS 对象确实包含 toJS()
方法,该方法会返回普通 JavaScript 数据结构形式的对象,但这种方法非常慢,广泛使用将会失去 Immutable.JS 提供的性能优势。
一旦使用,Immutable.JS 将遍布整个代码库
一旦使用 Immutable.JS 封装数据,你必须使用 Immutable.JS 的 get()
和 getIn()
属性访问器来访问它。
这将会在整个代码库中传播 Immutable.JS,包括潜在组件,你可能不喜欢拥有这种外部依赖关系。你的整个代码库必须知道哪些应该是 Immutable.JS 对象,哪些不是。这也会使得当你想从应用程序中移除 Immutable.JS 变得非常困难。
如下面最佳实践部分所述,可以通过将应用程序逻辑与数据结构解耦来避免此问题。
没有解构或展开运算符(Spread Operators)
因为你必须通过 Immutable.JS 本身的 get()
和 getIn()
方法来访问你的数据,所以你不能再使用 JavaScript 的解构运算符(或者提案中的 Object 扩展运算符),这使得你的代码更加冗余。
不适用于经常改变的小数值
Immutable.JS 最适用于数据集合,越大越好。当你的数据包含大量小而简单的 JavaScript 对象时,速度会很慢,每个对象都包含几个基本数据类型的 key。
注意:无论如何,这都不适用于 Redux state tree,该树通常为大量数据的集合。
难以调试
Immutable.JS 对象,如 Map
,List
等可能很难调试,因为检查这样的对象会看到整个嵌套层级结构,这些层级是你不关心的 Immutable.JS 特定的属性,而且你真正关心的是实际数据被封装了几层。
要解决此问题,请使用浏览器扩展程序,如 Immutable.js 对象格式化扩展,它在 Chrome 开发工具中显示数据,并在检查数据时隐藏 Immutable.JS 的属性。
破坏对象引用,导致性能较差
不可变的一个主要优点是它可以浅层平等检查,大大提高了性能。
如果两个不同的变量引用同一个不可变对象,那么对这两个变量进行简单的相等检查就足以确定它们是否相等,并且它们所引用的对象是不可变的。等式检查从不必检查任何对象属性的值,因为它是不可变的。
然而,如果封装在 Immutable.JS 对象中的数据本身就是一个对象,渐层检查起不到任何作用。这是因为 Immutable.JS 的 toJS()
方法会将 Immutable.JS 对象中的数据作为 JavaScript 值并返回,每次调用它时都会创建一个新对象,并且使用封装数据来分解引用。
因此,如果调用 toJS()
两次,并将结果赋值给两个不同的变量将导致这两个变量的等式检查失败,即时对象值本身没有改变。
如果在包装组件的 mapStateToProps
函数中使用 toJS()
,这就是一个特殊的问题了,因为 React-Redux 对返回的 props 对象中的每个值都进行了简单的比较。例如,下面代码中的 mapStateToProps
返回的 todos
prop 所引用的值将始终是不同的对象,因此无法通过渐层等式检查。
// 避免在 mapStateToProps 中使用 .toJS()
function mapStateToProps(state) {
return {
todos: state.get('todos').toJS() // 总为新对象
}
}
当浅层检查失败时,React-Redux 将导致组件重新渲染。因此,在 mapStateToProps
中使用 toJS()
的方式,将导致组件重新渲染,即时值未发生变化,也会严重影响性能。
该问题可以通过在高阶组件中使用 toJS()
来避免,如下面最佳实践部分所述。
更多信息
文章
- Immutable.js,持续化数据结构与结构共享
- 不可变的数据结构与 JavaScript
- React.js 纯粹渲染性能反面模式(anti-pattern)
- 使用 React 和 Redux 构建高效的用户界面
Chrome 扩展程序
- Immutable 对象格式化扩展
Immutable.JS 是否值得使用?
通常来说,是的。有各种各样的权衡和意见参考,但有很多很好的理由推荐使用。不要低估尝试追踪无意间突变的 state tree 中的属性的难度。
组件在不必要时重新渲染,在它必要时拒绝渲染,以及追踪致使出现渲染问题的错误都是非常困难的,因为渲染不正确的组件不一定是属性突变的组件。
这个问题主要是由 Redux 的 reducer 返回一个突变的 state 对象引起的。使用 Immutable.JS,此类问题根本不会出现,因此,你的应用程序中就排除了这类错误。
以上这些与它的性能以及丰富的数据操作 API 组合在一起,就是为什么值得使用 Immutable.JS 的原因了。
更多信息
文档
- 排错:dispatch action 后什么也没有发生
在 Redux 中使用 Immutable.JS 有哪些最佳实践?
Immutable.JS 可以为你的应用程序提供可靠性和显著的性能优化,但必须正确使用。如果你选择使用 Immutable.JS(记住,并不是必须使用它,还有其他不可变库可以使用),请遵循这些有见地的最佳实践,你将能充分利用它,从而不会被它可能导致的任何问题绊倒。
永远不要将普通的 JavaScript 对象与 Immutable.JS 混合使用
永远不要让一个普通的 JavaScript 对象包含 Immutable.JS 属性。同样,永远不要让 Immutable.JS 对象包含一个普通的 JavaScript 对象。
更多信息
文章
- 不可变的数据结构与 JavaScript
使整个 Redux state tree 成为 Immutable.JS 对象
对于使用 Redux 的应用程序来说,你的整个 state tree 应该是 Immutable.JS 对象,根本不需要使用普通的 JavaScript 对象。
使用 Immutable.JS 的
fromJS()
函数创建树。使用
combineReducers
函数的 Immutable.JS 的感知版本,比如 redux-immutable 中的版本,因为 Redux 本身会将 state tree 变成一个普通的 JavaScript 对象。当使用 Immutable.JS 的
update
,merge
或set
方法将一个 JavaScript 对象添加到一个 Immutable.JS 的 Map 或者 List 中时,要确保被添加的对象事先使用了fromJS()
转为一个 Immutable 的对象。
示例
// 避免
const newObj = { key: value }
const newState = state.setIn(['prop1'], newObj)
// newObj 作为普通的 JavaScript 对象,而不是 Immutable.JS 的 Map 类型。
// 推荐
const newObj = { key: value }
const newState = state.setIn(['prop1'], fromJS(newObj))
// newObj 现在是 Immutable.JS 的 Map 类型。
更多信息
文章
- 不可变的数据结构与 JavaScript
库
- redux-immutable
在除了 Dumb 组件外的组件使用 Immutable.JS
在任何地方使用 Immutable.JS 都可以保证代码的高性能。在你的 smart 组件中,选择器中,saga 或 thunk 中,action 创建函数 中,特别是你的 reducer 中都可以使用它。
但是,请不要在你的 Dumb 组件中使用 Immutable.JS。
更多信息
文章
- 不可变的数据结构与 JavaScript
- React 中的 Smart 和 Dumb 组件
限制对 toJS()
的使用
toJS()
是一个昂贵(性能)的函数,并且与使用 Immutable.JS 的目的相违背。避免使用它。
更多信息
议题
- Lee Byron 的 Twitter: "Perf tip for #immutablejs…"
你的选择器应该返回 Immutable.JS 对象
这种做法可以带来以下几个好处:
- 它避免了在选择器中调用
.toJS()
导致的不必要的重新渲染(因为.toJS()
将始终返回一个新对象)。- 虽然可以在调用
.toJS()
的地方对选择器进行缓存,但是如果返回的是 immutable.js 对象,就不需要多余的缓存。
- 虽然可以在调用
- 为选择器建立一致的接口; 您将不必考虑返回的是 Immutable.js 对象还是纯 JavaScript 对象。
在 Smart 组件中使用 Immutable.JS 对象
通过 React Redux 的 connect
函数访问 store 的 Smart 组件必须使用 Immutable.JS 作为选择器的返回值。以确保你避免了由于不必要的组件重新渲染而导致的潜在问题。必要时使用库来记忆选择器(例如:reselect)。
更多信息
文档
- 技巧:计算衍生数据
- FAQ:Immutable 数据
- Reselect 文档:如何使用 Reselect 结合 Immutable.js?
文章
- Redux 模式和反面模式
库
- Reselect: Redux 的选择器库
绝对不要在 mapStateToProps
中使用 toJS()
使用 toJS()
将 Immutable.JS 对象转换为 JavaScript 对象时,每次都会返回一个新的对象。如果在 mapStateToProps
中执行此操作,则会导致组件在每次 state tree 更改时都认为该对象已更改,因此会触发不必要的重新渲染。
更多信息
文档
- FAQ: Immutable 数据
永远不要在你的 Dumb 组件中使用 Immutable.JS
你的 dumb 组件应该是纯粹的;也就是说,它们应该在给定相同的输入的情况下产生相同的输出,并不具有外部依赖性。如果你将这一一个组件作为 props 传递给一个 Immutable.JS 对象,那么你需要依赖 Immutable.JS 来提取 props 的值,并以其他的方式操纵它。
这种依赖性会导致组件不纯,使组件测试更加困难,并且使组件复用和重构变得非常困难。
更多信息
文章
- 不可变的数据结构与 JavaScript
- React 中的 Smart 和 Dumb 组件
- 更好的 Redux 体系结构的小贴士:企业规模的经验教训
使用高阶组件来转换从 Smart 组件的 Immutable.JS props 到 Dumb 组件的 JavaScript props
有些东西需要将 Smart 组件中的 Immutable.JS props 映射到 Dumb 组件中的纯 JavaScript props。这里的有些东西是指高阶组件(HOC),它只需从 Smart 组件中获取 Immutable.JS props,然后使用 toJS()
将它们转换为普通 JavaScript props,然后传递给你的 Dumb 组件。
下面是一个关于 HOC 的例子。为了方便您,还以 NPM 包的形式提供了类似的 HOC:with-immutable-props-to-js。
import React from 'react'
import { Iterable } from 'immutable'
export const toJS = WrappedComponent => wrappedComponentProps => {
const KEY = 0
const VALUE = 1
const propsJS = Object.entries(wrappedComponentProps).reduce(
(newProps, wrappedComponentProp) => {
newProps[wrappedComponentProp[KEY]] = Iterable.isIterable(
wrappedComponentProp[VALUE]
)
? wrappedComponentProp[VALUE].toJS()
: wrappedComponentProp[VALUE]
return newProps
},
{}
)
return <WrappedComponent {...propsJS} />
}
以下为如何在 Smart 组件中使用它:
import { connect } from 'react-redux'
import { toJS } from './to-js'
import DumbComponent from './dumb.component'
const mapStateToProps = state => {
return {
// obj 是一个 Smart 组件中的不可变对象,
// 但它通过 toJS 被转换为普通 JavaScript 对象,并以纯 JavaScript 的形式传递给 Dumb 组件对象。
// 因为它在 mapStateToProps 中仍然是 Immutable.JS 对象,
// 虽然,这是无疑是错误重新渲染。
obj: getImmutableObjectFromStateTree(state)
}
}
export default connect(mapStateToProps)(toJS(DumbComponent))
通过在 HOC 中将 Immutable.JS 对象转换为纯 JavaScript 值,我们实现了 Dumb 的可移植性,也没在 Smart 组件中使用 toJS()
影响性能。
注意: 如果你的应用程序需要高性能,你可能需要完全避免使用 toJS()
,所以必须在你的 Dumb 组件中使用 Immutable.JS。但是,对于大多数应用程序来说并非如此,将 Immutable 保留在 Dumb 组件(可维护性,可移植性和更简单的测试)等方面的好处远远超过了保持它任何方面性能优化。
另外,在高阶组件中使用 toJS
应该不会引起任何性能的下降,因为只有在 connect 组件的 props 改变时才会调用组件。与任何性能问题一样,在决定优化什么之前先进行性能检测。
更多信息
文档
- React:高阶组件
文章
- 深入了解 React 的高阶组件
议题
- Reddit: acemarke 和 cpsubrian 对 Dan Abramov 的评论:Redux 不是一种架构或设计模式,它只是一个库。
Gists
- cpsubrian: React decorators for redux/react-router/immutable ‘smart’ components
使用不可变对象格式化 Chrome 扩展来辅助调试
安装 Immutable 对象格式化扩展,并检查你的 Immutable.JS 数据,而不会看到 Immutable.JS 本身的对象属性混淆视听。
更多信息
Chrome 扩展
- Immutable 对象格式化扩展