# 結合 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
- 排錯