# 结合 Immutable.JS 使用 Redux
## 目录
- [为什么应该使用 Immutable.JS 等不可变的库?](#why-use-immutable-library)
- [为什么应该选择 Immutable.JS 作为不可变的库?](#why-choose-immutable-js)
- [使用 Immutable.JS 有什么问题?](#issues-with-immutable-js)
- [Immutable.JS 是否值得使用?](#is-immutable-js-worth-effort)
- [在 Redux 中使用 Immutable.JS 有哪些最佳实践?](#immutable-js-best-practices)
<a id="why-use-immutable-library"></a>
## 为什么应该使用 Immutable.JS 等不可变的库?
Immutable.JS 不可变的库被设计旨在解决 JavaScript 中固有的不可变(Immutability)问题,为应用程序提供不可变带来的所有好处。
你是选择使用这样的库,还是坚持使用简单的 JavaScript,完全取决于你对向应用程序中添加另一个依赖是满意程度,或者取决于你是否确信使用它可以避免 JavaScript 处理不可变的方法中固有的缺陷。
无论你做何选择,请确保你熟悉[不可变,副作用和突变](http://cn.redux.js.org/docs/recipes/reducers/PrerequisiteConcepts.html#note-on-immutability-side-effects-and-mutation)的概念。尤其要确保你深入了解 JavaScript 在更新和复制值时所做的操作,以防止意外的突变(mutation)导致应用程序性能的降低,甚至完全破坏应用程序的性能。
#### 更多信息
**文档**
- [技巧:不可变,副作用和突变](http://cn.redux.js.org/docs/recipes/reducers/PrerequisiteConcepts.html#note-on-immutability-side-effects-and-mutation)
**文章**
- [Immutable.js 和函数式编程概念介绍](https://auth0.com/blog/intro-to-immutable-js/)
- [React.js 使用不可变的优点和缺点](http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/)
<a id="why-choose-immutable-js"></a>
## 为什么应该选择 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 内部通过[巧妙共享数据结构](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2#.z1g1ofrsi)避免了这种情况,最大限度地减少了拷贝数据的情况。它还能执行复杂的操作链,而不会产生不必要的(且昂贵的)中间数据克隆,这些数据很快就会被丢弃。
你决不会看到这些,当然 - 你给 Immutable.JS 对象的数据永远不会发生变化。但是,它从 Immutable.JS 中生成的 _intermediate_ 数据,可以通过链式调用序列中的数据进行自由的变换。因此,你可以拥有不可变数据结构的所有优势,并且不会产生任何潜在的(或很少)性能问题。
#### 更多信息
**文章**
- [Immutable.js,持续化数据结构与结构共享](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2#.6nwctunlc)
- [PDF: JavaScript Immutability - 不要更改](https://www.jfokus.se/jfokus16/preso/JavaScript-Immutability--Dont-Go-Changing.pdf)
**库**
- [Immutable.js](https://facebook.github.io/immutable-js/)
<a id="issues-with-immutable-js"></a>
## 使用 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 变得非常困难。
如下面[最佳实践部分](#immutable-js-best-practices)所述,可以通过将[应用程序逻辑与数据结构解耦](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2#.z1g1ofrsi)来避免此问题。
### 没有解构或展开运算符(Spread Operators)
因为你必须通过 Immutable.JS 本身的 `get()` 和 `getIn()` 方法来访问你的数据,所以你不能再使用 JavaScript 的解构运算符(或者提案中的 Object 扩展运算符),这使得你的代码更加冗余。
### 不适用于经常改变的小数值
Immutable.JS 最适用于数据集合,越大越好。当你的数据包含大量小而简单的 JavaScript 对象时,速度会很慢,每个对象都包含几个基本数据类型的 key。
注意:无论如何,这都不适用于 Redux state tree,该树通常为大量数据的集合。
### 难以调试
Immutable.JS 对象,如 `Map`,`List` 等可能很难调试,因为检查这样的对象会看到整个嵌套层级结构,这些层级是你不关心的 Immutable.JS 特定的属性,而且你真正关心的是实际数据被封装了几层。
要解决此问题,请使用浏览器扩展程序,如 [Immutable.js 对象格式化扩展](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog),它在 Chrome 开发工具中显示数据,并在检查数据时隐藏 Immutable.JS 的属性。
### 破坏对象引用,导致性能较差
不可变的一个主要优点是它可以浅层平等检查,大大提高了性能。
如果两个不同的变量引用同一个不可变对象,那么对这两个变量进行简单的相等检查就足以确定它们是否相等,并且它们所引用的对象是不可变的。等式检查从不必检查任何对象属性的值,因为它是不可变的。
然而,如果封装在 Immutable.JS 对象中的数据本身就是一个对象,渐层检查起不到任何作用。这是因为 Immutable.JS 的 `toJS()` 方法会将 Immutable.JS 对象中的数据作为 JavaScript 值并返回,每次调用它时都会创建一个新对象,并且使用封装数据来分解引用。
因此,如果调用 `toJS()` 两次,并将结果赋值给两个不同的变量将导致这两个变量的等式检查失败,即时对象值本身没有改变。
如果在包装组件的 `mapStateToProps` 函数中使用 `toJS()`,这就是一个特殊的问题了,因为 React-Redux 对返回的 props 对象中的每个值都进行了简单的比较。例如,下面代码中的 `mapStateToProps` 返回的 `todos` prop 所引用的值将始终是不同的对象,因此无法通过渐层等式检查。
```js
// 避免在 mapStateToProps 中使用 .toJS()
function mapStateToProps(state) {
return {
todos: state.get('todos').toJS() // 总为新对象
}
}
```
当浅层检查失败时,React-Redux 将导致组件重新渲染。因此,在 `mapStateToProps` 中使用 `toJS()` 的方式,将导致组件重新渲染,即时值未发生变化,也会严重影响性能。
该问题可以通过在高阶组件中使用 `toJS()` 来避免,如下面[最佳实践部分](#immutable-js-best-practices)所述。
#### 更多信息
**文章**
- [Immutable.js,持续化数据结构与结构共享](https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2#.hzgz7ghbe)
- [不可变的数据结构与 JavaScript](http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript)
- [React.js 纯粹渲染性能反面模式(anti-pattern)](https://medium.com/@esamatti/react-js-pure-render-performance-anti-pattern-fb88c101332f#.9ucv6hwk4)
- [使用 React 和 Redux 构建高效的用户界面](https://www.toptal.com/react/react-redux-and-immutablejs)
**Chrome 扩展程序**
- [Immutable 对象格式化扩展](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog)
<a id="is-immutable-js-worth-effort"></a>
## Immutable.JS 是否值得使用?
通常来说,是的。有各种各样的权衡和意见参考,但有很多很好的理由推荐使用。不要低估尝试追踪无意间突变的 state tree 中的属性的难度。
组件在不必要时重新渲染,在它必要时拒绝渲染,以及追踪致使出现渲染问题的错误都是非常困难的,因为渲染不正确的组件不一定是属性突变的组件。
这个问题主要是由 Redux 的 reducer 返回一个突变的 state 对象引起的。使用 Immutable.JS,此类问题根本不会出现,因此,你的应用程序中就排除了这类错误。
以上这些与它的性能以及丰富的数据操作 API 组合在一起,就是为什么值得使用 Immutable.JS 的原因了。
#### 更多信息
**文档**
- [排错:dispatch action 后什么也没有发生](https://cn.redux.js.org/docs/Troubleshooting.html#nothing-happens-when-i-dispatch-an-action)
<a id="immutable-js-best-practices"></a>
## 在 Redux 中使用 Immutable.JS 有哪些最佳实践?
Immutable.JS 可以为你的应用程序提供可靠性和显著的性能优化,但必须正确使用。如果你选择使用 Immutable.JS(记住,并不是必须使用它,还有其他不可变库可以使用),请遵循这些有见地的最佳实践,你将能充分利用它,从而不会被它可能导致的任何问题绊倒。
### 永远不要将普通的 JavaScript 对象与 Immutable.JS 混合使用
永远不要让一个普通的 JavaScript 对象包含 Immutable.JS 属性。同样,永远不要让 Immutable.JS 对象包含一个普通的 JavaScript 对象。
#### 更多信息
**文章**
- [不可变的数据结构与 JavaScript](http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript)
### 使整个 Redux state tree 成为 Immutable.JS 对象
对于使用 Redux 的应用程序来说,你的整个 state tree 应该是 Immutable.JS 对象,根本不需要使用普通的 JavaScript 对象。
- 使用 Immutable.JS 的 `fromJS()` 函数创建树。
- 使用 `combineReducers` 函数的 Immutable.JS 的感知版本,比如 [redux-immutable](https://www.npmjs.com/package/redux-immutable) 中的版本,因为 Redux 本身会将 state tree 变成一个普通的 JavaScript 对象。
- 当使用 Immutable.JS 的 `update`,`merge` 或 `set` 方法将一个 JavaScript 对象添加到一个 Immutable.JS 的 Map 或者 List 中时,要确保被添加的对象事先使用了 `fromJS()` 转为一个 Immutable 的对象。
**示例**
```js
// 避免
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](http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript)
**库**
- [redux-immutable](https://www.npmjs.com/package/redux-immutable)
### 在除了 Dumb 组件外的组件使用 Immutable.JS
在任何地方使用 Immutable.JS 都可以保证代码的高性能。在你的 smart 组件中,选择器中,saga 或 thunk 中,action 创建函数 中,特别是你的 reducer 中都可以使用它。
但是,请不要在你的 Dumb 组件中使用 Immutable.JS。
#### 更多信息
**文章**
- [不可变的数据结构与 JavaScript](http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript)
- [React 中的 Smart 和 Dumb 组件](http://jaketrent.com/post/smart-dumb-components-react/)
### 限制对 `toJS()` 的使用
`toJS()` 是一个昂贵(性能)的函数,并且与使用 Immutable.JS 的目的相违背。避免使用它。
#### 更多信息
**议题**
- [Lee Byron 的 Twitter: "Perf tip for #immutablejs…"](https://twitter.com/leeb/status/746733697093668864)
### 你的选择器应该返回 Immutable.JS 对象
总是返回 Immutable.JS 对象。
### 在 Smart 组件中使用 Immutable.JS 对象
通过 React Redux 的 `connect` 函数访问 store 的 Smart 组件必须使用 Immutable.JS 作为选择器的返回值。以确保你避免了由于不必要的组件重新渲染而导致的潜在问题。必要时使用库来记忆选择器(例如:reselect)。
#### 更多信息
**文档**
- [技巧:计算衍生数据](http://cn.redux.js.org/docs/recipes/ComputingDerivedData.html)
- [FAQ:Immutable 数据](/faq/ImmutableData.html#immutability-issues-with-react-redux)
- [Reselect 文档:如何使用 Reselect 结合 Immutable.js?](https://github.com/reduxjs/reselect/#q-how-do-i-use-reselect-with-immutablejs)
**文章**
- [Redux 模式和反面模式](https://tech.affirm.com/redux-patterns-and-anti-patterns-7d80ef3d53bc#.451p9ycfy)
**库**
- [Reselect: Redux 的选择器库](https://github.com/reduxjs/reselect)
### 绝对不要在 `mapStateToProps` 中使用 `toJS()`
使用 `toJS()` 将 Immutable.JS 对象转换为 JavaScript 对象时,每次都会返回一个新的对象。如果在 `mapStateToProps` 中执行此操作,则会导致组件在每次 state tree 更改时都认为该对象已更改,因此会触发不必要的重新渲染。
#### 更多信息
**文档**
- [FAQ: Immutable 数据](http://cn.redux.js.org/docs/faq/ImmutableData.html#how-can-immutability-in-mapstatetoprops-cause-components-to-render-unnecessarily)
### 永远不要在你的 Dumb 组件中使用 Immutable.JS
你的 dumb 组件应该是纯粹的;也就是说,它们应该在给定相同的输入的情况下产生相同的输出,并不具有外部依赖性。如果你将这一一个组件作为 props 传递给一个 Immutable.JS 对象,那么你需要依赖 Immutable.JS 来提取 props 的值,并以其他的方式操纵它。
这种依赖性会导致组件不纯,使组件测试更加困难,并且使组件复用和重构变得非常困难。
#### 更多信息
**文章**
- [不可变的数据结构与 JavaScript](http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript)
- [React 中的 Smart 和 Dumb 组件](http://jaketrent.com/post/smart-dumb-components-react/)
- [更好的 Redux 体系结构的小贴士:企业规模的经验教训](https://hashnode.com/post/tips-for-a-better-redux-architecture-lessons-for-enterprise-scale-civrlqhuy0keqc6539boivk2f)
### 使用高阶组件来转换从 Smart 组件的 Immutable.JS props 到 Dumb 组件的 JavaScript props
有些东西需要将 Smart 组件中的 Immutable.JS props 映射到 Dumb 组件中的纯 JavaScript props。这里的有些东西是指高阶组件(HOC),它只需从 Smart 组件中获取 Immutable.JS props,然后使用 `toJS()` 将它们转换为普通 JavaScript props,然后传递给你的 Dumb 组件。
这是一个关于 HOC 的例子:
```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 组件中使用它:
```js
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:高阶组件](https://facebook.github.io/react/docs/higher-order-components.html)
**文章**
- [深入了解 React 的高阶组件](https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.dw2qd1o1g)
**议题**
- [Reddit: acemarke 和 cpsubrian 对 Dan Abramov 的评论:Redux 不是一种架构或设计模式,它只是一个库。](https://www.reddit.com/r/javascript/comments/4rcqpx/dan_abramov_redux_is_not_an_architecture_or/d5rw0p9/?context=3)
**Gists**
- [cpsubrian: React decorators for redux/react-router/immutable ‘smart’ components](https://gist.github.com/cpsubrian/79e97b6116ab68bd189eb4917203242c#file-tojs-js)
### 使用不可变对象格式化 Chrome 扩展来辅助调试
安装 [Immutable 对象格式化扩展](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog),并检查你的 Immutable.JS 数据,而不会看到 Immutable.JS 本身的对象属性混淆视听。
#### 更多信息
**Chrome 扩展**
- [Immutable 对象格式化扩展](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog)
- 自述
- 介绍
- 动机
- 核心概念
- 三大原则
- 先前技术
- 学习资源
- 生态系统
- 示例
- 基础
- Action
- Reducer
- Store
- 数据流
- 搭配 React
- 示例:Todo List
- 高级
- 异步 Action
- 异步数据流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 迁移到 Redux
- 使用对象展开运算符
- 减少样板代码
- 服务端渲染
- 编写测试
- 计算衍生数据
- 实现撤销重做
- 子应用隔离
- 组织 Reducer
- Reducer 基础概念
- Reducer 基础结构
- Reducer 逻辑拆分
- Reducer 重构示例
- combineReducers 用法
- combineReducers 进阶
- State 范式化
- 管理范式化数据
- Reducer 逻辑复用
- 不可变更新模式
- 初始化 State
- 结合 Immutable.JS 使用 Redux
- 常见问题
- 综合
- Reducer
- 组织 State
- 创建 Store
- Action
- 不可变数据
- 代码结构
- 性能
- 设计哲学
- React Redux
- 其它
- 排错
- 词汇表
- API 文档
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文档
- API
- 排错