歡迎訪問個人博客Immutable.js與React,Redux及reselect的實踐javascript
本篇文章將聚焦Immutable與Redux,reselect的項目實踐,將從多方面闡述Immutable及Redux:包括什麼是Immutable,爲何須要使用Immutable,Immutable.js與React,Redux及reselect的組合實踐及優化,最後總結使用Immutable可能遇到的一些問題及解決方式。html
Immutable來自於函數式編程的世界,咱們能夠稱它爲不可變,試想以下代碼:java
var object = { x:1, y: 2 };
var object2 = { x: 1, y: 2 };
object == object2// false
object === object2 // false複製代碼
相等性檢查將包括兩個部分:react
JavaScript的對象是一個很是複雜的數據結構,它的鍵能夠指向任意值,包括object。JavaScript建立的對象將存儲在計算機內存中(對應一個物理地址),而後它返回一個引用,JavaScript引擎經過該引用能夠訪問該對象,該引用賦值給某個變量後,咱們即可以經過該變量以引用的方式操做該對象。引用檢查即檢查兩個對象的引用地址是否相同。git
層層循環檢查對象各屬性值是否相同。github
React經過對組件屬性(props)和狀態(state)進行變動檢查以決定是否更新並從新渲染該組件,若組件狀態太過龐大,組件性能就會降低,由於對象越複雜,其相等性檢查就會越慢。編程
Immutable提供一直簡單快捷的方式以判斷對象是否變動,對於React組件更新和從新渲染性能能夠有較大幫助。redux
Never mutated, instead copy it and then make change.api
絕對不要忽然修改對象,首先複製而後修改複製對象,再返回這個新對象,保持原對象不變。數組
Immutable對象和原生JavaScript對象的主要差別能夠歸納爲如下兩點:
持久數據結構主張全部操做都返回該數據結構的更新副本,並保持原有結構不變,而不是改變原來的結構。一般利用Trie構建它不可變的持久性數據結構,它的總體結構能夠看做一棵樹,一個樹節點能夠對應表明對象某一個屬性,節點值即屬性值。
一旦建立一個Immutable Trie型對象,咱們能夠把該Trie型對象想象成以下一棵樹,在以後的對象變動儘量的重用樹節點:
當咱們要更新一個Immutable對象的屬性值時,就是對應着須要重構該Trie樹中的某一個節點,對於Trie樹,咱們修改某一節點只須要重構該節點及受其影響的節點,即其祖先節點,如上圖中的四個綠色節點,而其餘節點能夠徹底重用。
上一節簡單介紹了什麼是Immutable,本節介紹爲何須要使用Immutable。
咱們不鼓勵忽然變動對象,由於那一般會打斷時間旅行及bug相關調試,而且在react-redux的connect
方法中狀態突變將致使組件性能低下:
connect
方法將檢查mapStateToProps
方法返回的props對象是否變動以決定是否須要更新組件。爲了提升這個檢查變動的性能,connect
方法基於Immutabe狀態對象進行改進,使用淺引用相等性檢查來探測變動。這意味着對對象或數組的直接變動將沒法被探測,致使組件沒法更新。在reducer函數中的諸如生成惟一ID或時間戳的其餘反作用也會致使應用狀態不可預測,難以調試和測試。
若Redux的某一reducer函數返回一個能夠突變的狀態對象,意味着咱們不能追蹤,預測狀態,這可能致使組件發生多餘的更新,從新渲染或者在須要更新時沒有響應,也會致使難以跟蹤調試bug。Immutable.js能提供一種Immutable方案解決如上提到的問題,同時其豐富的API也足夠支撐咱們複雜的開發。
Immutable能給咱們的應用提供較大的性能提高,可是咱們必須正確的使用它,不然得不償失。目前關於Immutable已經有一些類庫,對於React應用,首選的是Immutable.js。
首先須要明白的是React組件狀態必須是一個原生JavaScript對象,而不能是一個Immutable對象,由於React的setState
方法指望接受一個對象而後使用Object.assign
方法將其與以前的狀態對象合併。
class Component extends React.Component {
Constructor (props) {
super(props)
this.state = {
data: Immutable.Map({
count:0,
todos: List()
})
}
this.handleAddItemClick = this.handleAddItemClick.bind(this)
}
handleAddItemClick () {
this.setState(({data}) => {
data: data.update('todos', todos => todos.push(data.get('count')))
})
}
render () {
const data = this.state.data;
Return (
<div>
<button onclick={this.handleAddItemClick}></button>
<ul>
{data.get('todos').map(item =>
<li>Saved:
{item}</li>
)}
</ul>
</div>
)
}
}複製代碼
使用Immutable.js的訪問API訪問state,如get()
,getIn()
;
使用Immutable.js的集合操做生成組件子元素:
使用高階函數如map()
,reduce()
等建立React元素的子元素:
{data.get('todos').map(item =>
<li>Saved:
{item}</li>
)}複製代碼
使用Immutable.js的更新操做API更新state;
this.setState(({data}) => ({
data: data.update('count', v => v + 1)
}))複製代碼
或者
this.setState(({data}) => ({
data: data.set('count', data.get('count') + 1)
}));複製代碼
參考:
React自己是專一於視圖層的一個JavaScript類庫,因此其單獨使用時狀態通常不會過於複雜,因此其和Immutable.js的協做比較簡單,更重要也是咱們須要更多關注的地方是其與React應用狀態管理容器的協做,下文就Immutable.js如何高效的與Redux協做進行闡述。
咱們在Redux中講狀態(state)主要是指應用狀態,而不是組件狀態。
原始Redux的combineReducers
方法指望接受原生JavaScript對象而且它把state做爲原生對象處理,因此當咱們使用createStore
方法而且接受一個Immutable對象做應用初始狀態時,reducer
將會返回一個錯誤,源代碼以下:
if (!isPlainObject(inputState)) {
return (
`The ${argumentName} has unexpected type of "` + ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
".Expected argument to be an object with the following +
`keys:"${reducerKeys.join('", "')}"`
)
}複製代碼
如上代表,原始類型reducer接受的state參數應該是一個原生JavaScript對象,咱們須要對combineReducers
其進行加強,以使其能處理Immutable對象,redux-immutable 便是用來建立一個能夠和Immutable.js協做的Redux combineReducers。
const StateRecord = Immutable.Record({
foo: 'bar'
});
const rootReducer = combineReducers({
first: firstReducer
}, StateRecord);複製代碼
若是在項目中使用了react-router-redux類庫,那麼咱們須要知道routeReducer不能處理Immutable,咱們須要自定義一個新的reducer:
import Immutable from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = Immutable.fromJS({
locationBeforeTransitions: null
});
export default (state = initialState, action) => {
if (action.type === LOCATION_CHANGE) {
return state.set('locationBeforeTransitions', action.payload);
}
return state;
};複製代碼
當咱們使用syncHistoryWithStore
方法鏈接history對象和store時,須要將routing負載轉換成一個JavaScript對象,以下傳遞一個selectLocationState
參數給syncHistoryWithStore
方法:
import { browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState (state) {
return state.get('routing').toJS();
}
});複製代碼
當使用Immutable.js和Redux協做開發時,能夠從以下幾方面思考咱們的實踐。
不要在Immutable對象中混用原生JavaScript對象;
當在Immutable對象內添加JavaScript對象時,首先使用fromJS()
方法將JavaScript對象轉換爲Immutable對象,而後使用update()
,merge()
,set()
等更新API對Immutable對象進行更新操做;
// avoid
const newObj = { key: value }
const newState = state.setIn(['prop1'], newObj)
// newObj has been added as a plain JavaScript object, NOT as an Immutable.JS Map
// recommended
const newObj = { key: value }
const newState = state.setIn(['prop1'], fromJS(newObj))複製代碼
使用Immutable對象表示完整的Redux狀態樹;
對於一個Redux應用,完整的狀態樹應該由一個Immutable對象表示,而沒有原生JavaScript對象。
使用fromJS()
方法建立狀態樹
狀態樹對象能夠是一個Immutable.Record或者任何其餘的實現了get
,set
,withMutations
方法的Immutable集合的實例。
使用redux-immutable庫調整combineReducers
方法使其能處理Immutable。
當使用Redux做React應用狀態管理容器時,咱們一般將組件分爲容器組件和展現型組件,Immutable與Redux組件的實踐也主要圍繞這二者。
除了在展現型組件內,其餘地方一概使用Immutable方式操做狀態對象;
爲了保證應用性能,在容器組件,選擇器(selectors),reducer函數,action建立函數,sagas和thunks函數內等全部地方均使用Immutable,可是不在展現型組件內使用。
在容器組件內使用Immutable
容器組件可使用react-redux提供的connect
方法訪問redux的store,因此咱們須要保證選擇器(selectors)老是返回Immutable對象,不然,將會致使沒必要要的從新渲染。另外,咱們可使用諸如reselect的第三方庫緩存選擇器(selectors)以提升部分情景下的性能。
toJS()
方法功能就是把一個Immutable對象轉換爲一個JavaScript對象,而咱們一般儘量將Immutable對象轉換爲JavaScript對象這一操做放在容器組件中,這也與容器組件的宗旨吻合。另外toJS
方法性能極低,應該儘可能限制該方法的使用,如在mapStateToProps
方法和展現型組件內。
絕對不要在mapStateToProps
方法內使用toJS()
方法
toJS()
方法每次會調用時都是返回一個原生JavaScript對象,若是在mapStateToProps
方法內使用toJS()
方法,則每次狀態樹(Immutable對象)變動時,不管該toJS()
方法返回的JavaScript對象是否實際發生改變,組件都會認爲該對象發生變動,從而致使沒必要要的從新渲染。
絕對不要在展現型組件內使用toJS()
方法
若是傳遞給某組件一個Immuatble對象類型的prop,則該組件的渲染取決於該Immutable對象,這將給組件的重用,測試和重構帶來更多困難。
當容器組件將Immutable類型的屬性(props)傳入展現型組件時,需使用高階組件(HOC)將其轉換爲原生JavaScript對象。
該高階組件定義以下:
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} />
}複製代碼
該高階組件內,首先使用Object.entries
方法遍歷傳入組件的props,而後使用toJS()
方法將該組件內Immutable類型的prop轉換爲JavaScript對象,該高階組件一般能夠在容器組件內使用,使用方式以下:
import { connect } from 'react-redux'
import { toJS } from './to-js'
import DumbComponent from './dumb.component'
const mapStateToProps = state => {
return {
// obj is an Immutable object in Smart Component, but it’s converted to a plain
// JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript
// object. Because it’s still an Immutable.JS object here in mapStateToProps, though,
// there is no issue with errant re-renderings.
obj:getImmutableObjectFromStateTree(state)
}
}
export default connect(mapStateToProps)(toJS(DumbComponent))複製代碼
這類高階組件不會形成過多的性能降低,由於高階組件只在被鏈接組件(一般即展現型組件)屬性變動時纔會被再次調用。你也許會問既然在高階組件內使用toJS()
方法必然會形成必定的性能降低,爲何不在展現型組件內也保持使用Immutable對象呢?事實上,相對於高階組件內使用toJS()
方法的這一點性能損失而言,避免Immutable滲透入展現型組件帶來的可維護性,可重用性及可測試性是咱們更應該看重的。
使用Redux管理React應用狀態時,mapStateToProps
方法做爲從Redux Store上獲取數據過程當中的重要一環,它必定不能有性能缺陷,它自己是一個函數,經過計算返回一個對象,這個計算過程一般是基於Redux Store狀態樹進行的,而很明顯的Redux狀態樹越複雜,這個計算過程可能就越耗時,咱們應該要可以儘量減小這個計算過程,好比重複在相同狀態下渲染組件,屢次的計算過程顯然是多餘的,咱們是否能夠緩存該結果呢?這個問題的解決者就是reselect,它能夠提升應用獲取數據的性能。
reselect的原理是,只要相關狀態不變,即直接使用上一次的緩存結果。
reselect經過建立選擇器(selectors),該函數接受一個state參數,而後返回咱們須要在mapStateToProps
方法內返回對象的某一個數據項,一個選擇器的處理能夠分爲兩個步驟:
接受state參數,根據咱們提供的映射函數數組分別進行計算,若是返回結果和上次第一步的計算結果一致,說明命中緩存,則不進行第二步計算,直接返回上次第二步的計算結果,不然繼續第二步計算。第一步的結果比較,一般僅僅是===
相等性檢查,性能是足夠的。
根據第一步返回的結果,計算,返回最終結果。
以TODO爲例,有以下選擇器函數:
import { createSelector } from 'reselect'
import { FilterTypes } from '../constants'
export const selectFilterTodos = createSelector(
[getTodos, getFilters],
(todos, filters) => {
switch(filters) {
case FilterTypes.ALL:
return todos;
case FilterTypes.COMPLETED:
return todos.filter((todo) => todo.completed)
default:
return todos
}
}
)複製代碼
如上,createSelector方法,接受兩個參數:
mapStateToProps
方法所需的數據;而後在mapStateToProps
內使用該選擇器函數,接受state參數:
const mapStateToProps = (state) => {
return {
todos: selectFilterTodos(state)
}
}複製代碼
上文中的映射函數,內容如:
const getTodos = (state) => {state.todos}
const getFilter = (state) => {state.filter}複製代碼
另外須要注意的是,傳入createSelector
的映射函數返回的狀態應該是不可變的,由於默認緩存命中檢測函數使用引用檢查,若是使用JavaScript對象,僅改變該對象的某一屬性,引用檢測是沒法檢測到屬性變動的,這將致使組件沒法響應更新。在緩存結果處理函數內執行以下代碼,是不行的:
todos.map(todo => {
todo.completed = !areAllMarked
return todo
})複製代碼
這種忽然性的改變某一狀態對象後,其差別檢測沒法經過,將命中緩存,沒法更新,在未使用Immutable.js庫時,應該採用以下這種方式:
todos.map(todo => Object.assign({}, todo, {
completed: !areAllMarked
}))複製代碼
老是返回一個新對象,而不影響原對象。
前面使用createSelector
方法建立的選擇器函數默認緩存間隔是1,只緩存上一次的計算結果,即選擇器處理流程的第一步,僅會將當前計算結果與緊鄰的上一次計算結果對比。
有時候也許咱們會想是否能夠加大緩存程度呢?好比當前狀態a,變化到狀態b,此時緩存的僅僅是狀態b下的選擇器計算結果,若是狀態再次變爲a,比對結果天然是false,依然會執行復雜的計算過程,那咱們是否能緩存第一次狀態a下的選擇器計算結果呢?答案就在createSelectorCreator
。
defaultMemoize(func, equalityCheck = defaultEqualityCheck)複製代碼
defaultMemoize將緩存傳遞的第一個函數參數func
的返回結果,該函數是使用createSelector
建立選擇器時傳入的緩存結果處理函數,其默認緩存度爲1。
equalityCheck
是建立的選擇器使用的緩存命中檢測函數,默認函數代碼如:
function defaultEqualityCheck(currentVal, previousVal) {
return currentVal === previousVal
}複製代碼
只是簡單的進行引用檢查。
createSelectorCreator
方法支持咱們建立一個自定義的createSelector
函數,而且支持咱們傳入自定義的緩存計算函數,覆蓋默認的defaultMemoize
函數,定義格式以下:
createSelectorCreator(memoize, ...memoizeOptions)複製代碼
memoize
參數是一個緩存函數,用以替代defaultMemoize
,該函數接受的第一個參數就是建立選擇器時傳入的緩存結果處理函數;…memoizeOptions
是0或多個配置對象,將傳遞給memoize
緩存函數做爲後續參數,如能夠傳遞一個自定義緩存檢測函數覆蓋defaultEqualityCheck
;// 使用lodash.isEqual覆蓋默認的‘===’引用等值檢測
import isEqual from 'lodash.isEqual'
import { createSelectorCreator, defaultMemoize } from 'reselect'
// 自定義選擇器建立函數
const customSelectorCreator = createSelectorCreator(
customMemoize, // 自定義緩存函數,也能夠直接使用defaultMemoize
isEqual, // 配置項
option2 // 配置項
)
// 自定義選擇器
const customSelector = customSelectorCreator(
input1, // 映射函數
input2, // 映射函數
resultFunc // 緩存結果處理函數
)
// 調用選擇器
const mapStateToProps = (state) => {
todos: customSelector(state)
}複製代碼
在自定義選擇器函數內部,會執行緩存函數:
customMemoize(resultFunc, isEqual, option2)複製代碼
如上文爲例,reselect是內在須要使用Immutable概念數據的,當咱們把整個Redux狀態樹Immutable化之後,須要進行一些修改。
修改映射函數:
const getTodos = (state) => {state.get('todos')}
const getFilter = (state) => {state.get('filter')}複製代碼
特別須要注意的是在選擇器第二步處理函數內,若是涉及Immutable操做,也須要額外修改爲Immutable對應方式。
不管什麼狀況,都不存在絕對完美的事物或者技術,使用Immutable.js也必然會帶來一些問題,咱們能作的則是儘可能避免或者盡最大可能的分化這些問題,而能夠更多的去發揚該技術帶來的優點,使用Immutable.js最多見的問題以下。
很難進行內部協做
Immutable對象和JavaScript對象之間存在的巨大差別,使得二者之間的協做一般較麻煩,而這也正是許多問題的源頭。
get
,getIn
等API方式;滲透整個代碼庫
Immutable代碼將滲透入整個項目,這種對於外部類庫的強依賴會給項目的後期帶來很大約束,以後若是想移除或者替換Immutable是很困難的。
不適合常常變動的簡單狀態對象
Immutable和複雜的數據使用時有很大的性能提高,可是對於簡單的常常變動的數據,它的表現並很差。
切斷對象引用將致使性能低下
Immutable最大的優點是它的淺比較能夠極大提升性能,當咱們屢次使用toJS
方法時,儘管對象實際沒有變動,可是它們之間的等值檢查不能經過,將致使從新渲染。更重要的是若是咱們在mapStateToProps
方法內使用toJS
將極大破壞組件性能,若是真的須要,咱們應該使用前面介紹的高階組件方式轉換。
難以調試
當咱們審查一個Immutable對象時,瀏覽器會打印出Immutable.js的整個嵌套結構,而咱們實際須要的只是其中小一部分,這致使咱們調試較困難,可使用Immutable.js Object Formatter瀏覽器插件解決。