iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 26
1
Modern Web

寫React的那些事系列 第 26

React Day26 - Immutablejs

在寫React的時候,常常會看到有人提到immutable.js,今天要來介紹這套library,了解原理後會覺得它真的是一套很棒的工具喔!介紹它之前要先說明一下javascript的mutable,在javascript中Number、String、Boolean等都是儲存值,但是Object和Array都是儲存reference,reference就是指向的記憶體位置,並不是真正的值,所以當複製Object或Array時,對複製後的值修改也會改到原本的值,這就是所謂的mutable。

以下範例可以直接在console執行看看,就會比較清楚mutable的特性:

const x = { a:1, b:2 };
// y複製的是x的reference
const y = x;
y.a = 10;

console.log(x.a);   // 10
console.log(y.a);   // 10

由於javascript mutable的特性,在網路上可以找到有滿多人實作deepClone、deepCopy:

function deepClone(obj) {
  if (!isObject(obj)) return obj;
  const cloneObj = isArray(obj) ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = obj[key];

      if (isObject(value)) {
        cloneObj[key] = deepClone(value);
      } else {
        cloneObj[key] = value;
      }
    }
  }
  return cloneObj;
}

可以從Demo看到實際執行後,Object跟Array都可以完全被複製值(deep clone),而不是複製reference。而deepClone雖然可以解決mutable的問題,讓我們在做Redux時,不用顧慮是否修改到prev state,但是一旦要更新Object就要整個複製,有點太浪費資源,所以Facebook工程師在2013年時就推出這套ImmutableJS

ImmutableJS


Immutable相對於mutable就是指說,一旦data被建立就不可以再改變,但是它又和deep clone不同,因為它只複製有變動的節點父層以上的地方。看下面這張圖會更清楚,當我們要變動F這個節點的資料時,會先copy這個節點,再往上copy它全部的父節點(A、C),然後其他沒有改變的節點依然指向原本的位置,這就是ImmutableJS的實作概念,有效地節省了不必要的浪費。

http://ithelp.ithome.com.tw/upload/images/20161226/20078318v8vCWkwHtH.png

Immutable API提供了許多 Persistent Immutable data structures ,像是ListStackMapOrderedMapSetOrderedSet還有Record,當我們在操作這些immutable data時,要記得它們 always yield new updated data ,透過API操作的時候,回傳的都會是新的data,所以要再指定回去。

ImmutableJS API


Immutable如剛剛提到的提供許多不同的資料型態,這邊我只簡單介紹幾個比較常用到的,有興趣看更多的人可以參考Immutable Doc

要使用之前,我們需要先安裝immutable package:

npm install immutable --save

fromJS()


Deeply converts plain JS objects and arrays to Immutable Maps and Lists.

我們在javascript中使用的Object跟Array,可以透過fromJS()幫我們把這些javascript mutable的資料型態轉換成immutable,Object轉換後會變成Map,Array轉換後會變成List,而且fromJS()會把資料內的每一層都做轉換。

使用範例:

const mutableArray = [1, 2, 3];
const immutableArray = Immutable.fromJS(mutableArray);

const mutableObject = { x: 1, y: 2 };
const immutableObject = Immutable.fromJS(mutableObject);

Map


Immutable Map可以從javascript Object轉換而來,也可以把它想成就是Object的替代。

set()、get()

  • 使用set(key, value)改變值,並且回傳一個新的Map。
  • 使用get(key)取值。

以下直接用範例說明:

// 使用fromJS轉換成Immutable Map
const map1 = Immutable.fromJS({a: 10, b: 20, c: 30});
const map2 = map1.set('a', 100);

console.log(map1.get('a')); // 10
console.log(map2.get('a')); // 100

Demo

setIn()、getIn()

  • 當要set或get的不是第一層的數值,必須使用 Deep persistent changes
  • 使用setIn(keyPath, value)改變值,並且回傳一個新的Map。
  • 使用get(keyPath)取值。
  • 這邊的keyPath可以是每一層深度的key組成的array。
// 使用fromJS轉換成Immutable Map
const map1 = Immutable.fromJS({
  name: 'Jack',
  profile: {
    age: 30,
    height: 170,
    weight: 70
  }
});
const map2 = map1.setIn(['profile', 'age'], 40);

console.log(map1.getIn(['profile', 'age'])); // 30
console.log(map2.getIn(['profile', 'age'])); // 40

Demo

List


Immutable List可以從javascript Array轉換而來,也可以把它想成就是Array的替代。

set()、get()

  • 使用set(index)改變值,並且回傳一個新的Map。
  • 使用get(index)取值。
const list1= Immutable.fromJS(['a', 'b', 'c']);
const list2 = list1.set(1, 'z');

console.log(list1.get(1)); // 'b'
console.log(list2.get(1)); // 'z'

Demo

setIn()、getIn()

  • 這這裡的概念和Map一樣,如果要 Deep persistent changes 必須使用這兩個API。
  • 使用setIn(keyPath, value)改變值,並且回傳一個新的Map。
  • 使用get(keyPath)取值。
const list1= Immutable.fromJS([ 'a', 'b', ['x', 'y', 'z'] ]);
const list2 = list1.setIn([2, 1], 'GJ');

console.log(list1.getIn([2, 1])); // 'y'
console.log(list2.getIn([2, 1])); // 'GJ'

Demo

List也有提供其他像Array對應的方法可以使用,EX:poppushshift...等,使用概念都相同,只有需要特別注意的是每次有修改裡面的值,不管是第一層還是deep層,都會回傳一個新的data回來,就像上面setSetIn那樣,因為我們現在的data已經是immutable囉!

Others


當我們在寫Redux時,可以透過使用ImmutableJS的幫助來操作data,這樣比較能確保prev state不會被修改到,但是如果我們把immutable的data log出來,可能會發現這樣的data很難看懂,這時候可以使用Map和List都有的toJS(),來把immutable切換回javascript的格式。

以下加上redux-logger來說明:

import { createStore, applyMiddleware } from 'redux';
import { Map } from 'immutable';
import createLogger from 'redux-logger';

import rootReducer from './reducers';

// 建立Map,指定給initialState
const initialState = Map({});
// 建立store時,把initialState也指定進去
const store = createStore(rootReducer, initialState, applyMiddleware(
  createLogger({
    stateTransformer: state => state.toJS()   // log前先轉換state.toJS()
  })
));

export default store;

在寫Redux的時候,會發現操作多層的Object是一件很辛苦的事情,尤其是複雜又多層的json,使用ImmutableJS是比deep copy更好的解法,另外,其實也可以參考normalizr
,它的意思就是把JSON格式扁平化,避免深層的JSON,我覺得是用另一種方式來處理這類問題,有興趣也可以看看它的概念再決定要用哪種方式來處理較複雜的Redux state格式問題噢!


上一篇
React Day25 - Async Action 與 redux-thunk
下一篇
React Day27 - ESLint(1)
系列文
寫React的那些事31

尚未有邦友留言

立即登入留言