天天看點

在react/redux中使用Immutable

有人說 Immutable 可以給 React 應用帶來數十倍的提升,也有人說 Immutable 的引入是近期 JavaScript 中偉大的發明,因為同期 React 太火,它的光芒被掩蓋了。這些至少說明 Immutable 是很有價值的,下面我們來一探究竟。

1、什麼是Immutable?

Immutable是一旦建立,就不能被更改的資料。對Immutable對象的任何修改或添加删除操作都會傳回一個新的Immutable對象。Immutable實作的原理是Persistent Data Structure(持久化資料結構),也就是是永久資料建立新資料時,要保證舊資料同時可用且不變。同時為了避免deepCopy把所有節點都複制一遍帶來的性能損耗,Immutable使用了Structural Sharing(結構共享),即如果對象樹結點發生變化,隻修改這個結點和受它影響的父節點,其他結點進行共享。

在react/redux中使用Immutable
在react/redux中使用Immutable
在react/redux中使用Immutable

初識:

讓我們看下面一段代碼:

function keyLog(touchFn) {
  let data = { key: 'value' };
  fun(data);
  console.log(data.key); // 猜猜會列印什麼?
}      

不檢視fun方法,不知道它對data做了什麼,無法确認會列印什麼。但如果data是Immutable,你可以确定列印的就是value:

function keyLog(touchFn) {
  let data = Immutable.Map({ key: 'value' });
  fun(data);
  console.log(data.get('key'));  // value
}      

JavaScript中的Object與Array等使用的是引用指派,如果新的對象簡單的引用了原始對象,改變新的對象也将影響舊的。

foo = {a:1};  bar = foo;  bar.a = 2;
foo.a // 2      

雖然這樣可以節約記憶體,但當應用複雜後,造成了狀态不可控,是很大的隐患,節約記憶體的優點變得得不償失。

Immutable則不一樣,相應的:

foo = Immutable.Map({ a: 1});
bar = foo.set('a', 2);
foo.get('a') // 1      

 簡潔:

在redux中,它的最優做法是每個reducer都傳回一個新的對象(數組),是以我們常常會看到這樣的代碼:

// reducer
...
return [
   ...oldArr.slice(0,3),
   newValue,
   ...oldArr.slice(4)   
];      

為了傳回新的對象(數組),不得不有上面奇怪的樣子,而在使用更深的資料結構時會變的更棘手。

讓我們看看Immutable的做法:

// reducer
...
return oldArr.set(4, newValue);      

是不是很簡潔?

關于"===":

衆所周知,對于Object與Array的===比較,是對引用位址的比較,而不是“值比較”,如:

{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false      

對于上面隻能采用deepCopy、deepCompare來比較,不僅麻煩而且耗性能。

我們感受一下Immutbale的做法:

map1 = Immutable.Map({a:1, b:2, c:3});
map2 = Immutable.Map({a:1, b:2, c:3});
Immutable.is(map1, map2); // true 比較值
map1 === map2;             // false 比較位址      
// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
List1 = Immutable.fromJS([1, 2, [3, 4]]);
List2 = Immutable.fromJS([1, 2, [3, 4]]);
Immutable.is(List1, List2); // true      

似乎有陣清風吹過。

Immutable.is

 比較的是兩個對象的 

hashCode

 或 

valueOf

(對于 JavaScript 對象)。由于 immutable 内部使用了 Trie 資料結構來存儲,隻要兩個對象的 

hashCode

 相等,值就是一樣的。這樣的算法避免了深度周遊比較,性能非常好。

Immutable使用了Structure Sharing會盡量複用記憶體,甚至以前使用的對象也可以再次被複用,引用的對象會被垃圾回收。

import { Map} from 'immutable';
let a = Map({
  select: 'users',
  filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');

a === b; // false
a.get('filter') === b.get('filter'); // true      

上面 a 和 b 共享了沒有變化的 

filter

 節點。

并發安全:

傳統的并發非常難做,因為要處理各種資料不一緻問題,是以『聰明人』發明了各種鎖來解決。但使用了 Immutable 之後,資料天生是不可變的,并發鎖就不需要了。

然而現在并沒什麼卵用,因為 JavaScript 還是單線程運作的啊。但未來可能會加入,提前解決未來的問題不也挺好嗎?

函數式程式設計:

Immutable本身就是函數式程式設計中的概念,純函數式程式設計比面向對象更适用于前端開發。因為隻要輸入一緻,輸出必然一緻,這樣開發的元件更易于調試群組裝。

像 ClojureScript,Elm 等函數式程式設計語言中的資料類型天生都是 Immutable 的,這也是為什麼 ClojureScript 基于 React 的架構 --- Om 性能比 React 還要好的原因。

2、在react中使用Immutable

熟悉React的都知道,React做性能優化時有個大招,就是使用shouldComponentUpdate(),但它預設傳回true,即始終會執行render()方法,後面做Virtual DOM比較,并得出是都需要做真是DOM更新,這裡往往會帶來很多務必要的渲染成為性能瓶頸。

在使用原生屬性時,為了得出shouldComponetUpdate正确的true or false,不得不用deepCopy、deepCompare來算出答案,但 deepCopy 和 deepCompare 一般都是非常耗性能的。

而在有了Immutable之後,Immutable 則提供了簡潔高效的判斷資料是否變化的方法,來減少 React 重複渲染,提高性能,隻需 

===

 和 

is

 比較就能知道是否需要執行 

render()

,而這個操作幾乎 0 成本,是以可以極大提高性能。修改後的 

shouldComponentUpdate

 是這樣的:

import { is } from 'immutable';

shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
  const thisProps = this.props || {}, thisState = this.state || {};

  if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
      Object.keys(thisState).length !== Object.keys(nextState).length) {
    return true;
  }

  for (const key in nextProps) {
    if (!is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (thisState[key] !== nextState[key] || !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}      

使用 Immutable 後,如下圖,當紅色節點的 state 變化後,不會再渲染樹中的所有節點,而是隻渲染圖中綠色的部分:

在react/redux中使用Immutable

setState 的一個技巧

React 建議把 

this.state

 當作 Immutable 的,因為修改前需要做一個 deepCopy,顯得麻煩:

import '_' from 'lodash';

const Component = React.createClass({
  getInitialState() {
    return {
      data: { times: 0 }
    }
  },
  handleAdd() {
    let data = _.cloneDeep(this.state.data);
    data.times = data.times + 1;
    this.setState({ data: data });
    // 如果上面不做 cloneDeep,下面列印的結果會是已經加 1 後的值。
    console.log(this.state.data.times);
  }
}      

使用 Immutable 後:

getInitialState() {
    return {
      data: Map({ times: 0 })
    }
  },
  handleAdd() {
    this.setState({ data: this.state.data.update('times', v => v + 1) });
    // 這時的 times 并不會改變
    console.log(this.state.data.get('times'));
  }      

上面的 

handleAdd

 可以簡寫成:

handleAdd() {
    this.setState(({data}) => ({
      data: data.update('times', v => v + 1) })
    });
  }      

3、如何在Redux中使用Immutable

目标:将

state

 -> Immutable化。

關鍵的庫:gajus/redux-immutable

将原來 Redux提供的combineReducers改由上面的庫提供: 

// rootReduers.js
// import { combineReducers } from 'redux'; // 舊的方法
import { combineReducers } from 'redux-immutable'; // 新的方法

import prop1 from './prop1';
import prop2 from './prop2';
import prop3 from './prop3';

const rootReducer = combineReducers({
  prop1, prop2, prop3,
});


// store.js
// 建立store的方法和正常一樣
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);
export default store;      

通過新的

combineReducers

将把store對象轉化成Immutable,在container中使用時也會略有不同(但這正是我們想要的):

const mapStateToProps = (state) => ({
  prop1: state.get('prop1'),
  prop2: state.get('prop2'),
  prop3: state.get('prop3'),
  next: state.get('next'),
});
export default connect(mapStateToProps)(App);      

4、總結

Immutable 可以給應用帶來極大的性能提升,但是否使用還要看項目情況。由于侵入性較強,新項目引入比較容易,老項目遷移需要評估遷移。對于一些提供給外部使用的公共元件,最好不要把 Immutable 對象直接暴露在對外接口中。

如果 JS 原生 Immutable 類型會不會太美,被稱為 React API 終結者的 Sebastian Markbåge 有一個這樣的提案,能否通過現在還不确定。不過可以肯定的是 Immutable 會被越來越多的項目使用。