Immutable源碼解析與性能優化

Immutable原理解析

簡介

what is Immutable

1.不可變,一成不變的node

2.對immutable數據的每次修改操做都會返回一個新的dataajax

掏出一副老生常談的圖算法

immutable的優勢

1.歷史回退(同時不浪費內存),時間旅行之類的easy!npm

2.函數式編程編程

3.下降代碼的複雜度api

數據類型

List: 類Array數組

Map:類Object/Map函數式編程

Set:類Set函數

OrderMap/Set:有序Map/Set 性能

....還有些不經常使用的數據類型

API

fromJS/toJS

對傳入對象或數組進行deepImmutable,array轉成List,Object轉成Map

const a = Immutable.fromJS({a:1,b:2})

console.log(a)    //Map {size: 2, _root: ArrayMapNode, __ownerID: undefined, __hash: 1014196085, __altered: false}

//定製化fromJS,根據key索引和value決定你想將他淺immutable仍是深immutable,或者轉換成其餘immutable類型
const b = Immutable.fromJS({a:['a','b'],b:2},(key,value)=>{

    const isIndexed = Immutable.Iterable.isIndexed(value);
    return isIndexed ? value.toList() : value.toOrderedMap();

})

a.toJS() // {a:1,b:2}

Map/List

Map

語法上同時兼容了ES6 Map,支持[key,value]形式傳入

const MapA = Immutable.Map([['a',1],['b','2']])

const MapB = Immutable,Map({a:1})

console.log(MapA.toJS(),MapB.toJS()) // {a:1,b:2} {a:1}

List

const ListA = Immutable.List([['a',1],['b','2']])

ListA.toJS() // [['a',1],['b','2']]

size

獲取大小

const ListA = Immutable.List([['a',1],['b','2']])

const MapA = Immutable.Map({a:{a:1}})

ListA.size // 2

MapA.size // 1

get/getIn

使用方式:get(key:any, notSetValue) / getIn(keyPath:array,notSeValue)

const obj = Immutable.fromJS({a:{a:8}})

console.log(obj.get('a'),obj.getIn(['a','a'])) //Map.... 8

console.log(obj.get('b','joker'),obj.getIn(['b','b','b'],'joker')) //joker joker

const array = Immutable.fromJS([{a:1},'2'])

array.get(0).toJS() // {a:1}

array.getIn([0,'a']) // 1

今後優雅寫代碼

之前的咱們
if(a && a.data && a.data.productList && a.data.productList.length > 0) 

如今的咱們
$immutable.getIn(['data','productList'],List()).size > 0

immutable除了對嵌套形式的數據進行分離外,對於同一層級的數據也進行了分割,見下文_tail+__root區間存儲

set/setIn

const ListA = Immutable.from({a:{a:1}})

const ListB = ListA.set('a',{o:77}) // {a:{o:77}}

ListB === ListA // false

ListA.setIn(['a','a'],'7777') // {a:{a:777}}

set/setIn是咱們最經常使用的api,其內部實現和update/updateIn同樣。也是immutable之因此immutable的核心所在

在文章剛開始提到的immutable原理圖中,爲何immutable在改變一個節點後,該父節點的鏈路上都變成了新的節點,一方面和實際須要有關,一方面也與set方法的實現有關。

從實際須要的角度,數據若是想immutable化,即先後徹底是兩個對象,同時爲了不deepClone的性能問題,達到不變數據內存的儘量複用。修改的節點和該父級鏈路上都變成新的對象顯然是最優方案。

從實現角度來講,咱們修改一個層級很深的節點,通常會調用immutable提供的setIn(['a','a'],xx)/update(['a','a'],xxx)這樣的方法。

實際immutable的整個一套修改流程是這樣的

假設咱們操做的數據是{a:{a:1}} 執行 setIn(['a','a'],'XXX')操做

['a','a']這是一個keyPath,immutable會按照順序一層層往裏找 找到指定節點那塊的時候,開始修改值 獲得一個修改完的{a:xxx}後,再原路向上set每一級,會先將每一級淺拷貝一遍,而後更新淺拷貝後的對象,將修改完的再吐給上一層,重複這樣的操做,最後返回了一個新的immutable對象

// 由於obj在immutable裏的存儲格式也是數組類型(類Map),因此也可使用arrCopy
function arrCopy(arr, offset) {
  offset = offset || 0;
  var len = Math.max(0, arr.length - offset);
  var newArr = new Array(len);
  for (var ii = 0; ii < len; ii++) {
    newArr[ii] = arr[ii + offset];
  }
  return newArr;
}
// 實際的更新邏輯
function updateInDeeply(
  inImmutable,
  existing,
  keyPath,
  i,
  notSetValue,
  updater
) {
  const wasNotSet = existing === NOT_SET;
  if (i === keyPath.length) {        //根據傳進的keyPath進行迭代
    const existingValue = wasNotSet ? notSetValue : existing;
    const newValue = updater(existingValue); 
    return newValue === existingValue ? existing : newValue;
  }
  if (!wasNotSet && !isDataStructure(existing)) {
    throw new TypeError(
      'Cannot update within non-data-structure value in path [' +
        keyPath.slice(0, i).map(quoteString) +
        ']: ' +
        existing
    );
  }
  const key = keyPath[i];
  const nextExisting = wasNotSet ? NOT_SET : get(existing, key, NOT_SET);    //get到每一層的Data
  const nextUpdated = updateInDeeply(
    nextExisting === NOT_SET ? inImmutable : isImmutable(nextExisting),
    nextExisting,
    keyPath,
    i + 1,
    notSetValue,
    updater
  );
  return nextUpdated === nextExisting
    ? existing
    : nextUpdated === NOT_SET
      ? remove(existing, key)
      : set(        //最核心的地方 將change後的結果set到每一層
          wasNotSet ? (inImmutable ? emptyMap() : {}) : existing,
          key,
          nextUpdated
        );
}

merge/mergeDeep

對對象進行merge,支持傳入immutable對象和普通對象

const objA = Immutable.fromJS({a:1,b:{a:2}})

const objB = Immutable.fromJS({a:3,b:{h:2}})

objA.merge({a:3,b:{h:2}}) // {a:3,b:{h:2}}

objA.merge(objB) // {a:3,b:{h:2}}

objA.mergeDeep({a:3,b:{h:2}}) // {a:3,b:{a:2,h:2}}


// 一般咱們reducer中對於action,state處理都會這樣

 return {
     ...state,
     ...action.payload
 }

// 如今咱們能夠這麼寫
 return state.merge(action.payload)

is

對兩個immutable對象進行diff

const immutableA = Immutable.fromJS({a:{a:1}})

const immutableB = immutableA.fromJS({a:{a:1}})

immutableA === immutableB // false

is(immutableA, immutableB) //true

is不支持淺immutable Data的對比,不支持普通對象的對比

經常使用操做

1.List:pop,push,shift,unshift,slice,forEach,Map,filter

與原生用法幾乎一致,可是有兩點須要注意:全部修改型操做一定返回一個新的Data。foreach是返回迭代數

Immutable.fromJS([1, 2, 3, 4, 5, {a: 123}]).forEach((value, index, array)=>{
    return value < 5;
}); // 5

2.Map:同時也支持forEach之類的遍歷,由於其存儲方式以Array存儲。特有方法的話mapKeys/mapEntries

經常使用api其實不想多說,網上有大把的資源 百度 必應 谷歌

Hash

將immutable對象hash化,在其屬性_hash上掛載,

const obj1 = immutable.fromJS({a:{a:1}})
const obj2 = immutable.Map({a:{a:1}})

Immutable.hash(obj1)

Immutable.hash(obj2)

obj1.__hash === obj2.__hash // false 具體原理見下文Hash原理剖析

withMutation&asMutable/asImutable

const ListA = Immutable.List(['a','b'])

ListA.push('gg')
    .pop()
    .shift()

按照immutable每一個操做一定返回新的對象的這種說法,上述代碼產生了不少冗餘的List,而針對這點immutable給出了兩種解決方案

//withMutation
const ListA = Immutable.List(['a','b'])

const ListB = ListA.withMutations(($list)=>{
    $list.push('gg')
        .pop()
        .shift()
})

//asMutable/asImutable
const ListA = Immutable.List(['a','b'])
const ListB = ListA.asMutable()

console.log(ListA === ListB,Immutable.is(ListA,ListB)) // false true

const ListC = ListB.pop()

console.log(ListB,ListC === ListB,Immutable.is(ListC,ListB)) // ['a'] true true

const ListFinally = ListC.asImmutable()    //asMutable/asImutable必須同時成對出現

而immutable是怎麼實現這個的呢??

仔細觀察immutable對象,嗯,你會發現有個__ownerID,嗯,而後呢,就沒有而後了。。。而後你就要看源碼了

//asMutable源碼
function asMutable() {
  return this.__ownerID ? this : this.__ensureOwner(new OwnerID());
}

//當咱們修改節點時都會相似觸發一個editableVNode這樣的函數
function editableVNode(node, ownerID) {
  if (ownerID && node && ownerID === node.ownerID) {
    return node;
  }
  return new VNode(node ? node.array.slice() : [], ownerID); //
}

經過實例函數的方式得到惟一ID,這點仍是很細膩的

immutable優勢及使用技巧

1.高效的存取方案 __root + __tail

若是說immutable他要轉換一個length 1000的array,他會怎麼作呢,存儲上他會將1000按length32爲單位進行存儲,放置在_root中,剩下的扔進_tail。同理,immutable在進行get/set操做時,扔進去一個索引100,首先作的事是,確認這個100在那個索引區,而後再去那個32的array中拿數據。

// List.set
let newTail = list._tail;
  let newRoot = list._root;
  const didAlter = MakeRef(DID_ALTER);
  if (index >= getTailOffset(list._capacity)) {
    newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter);
  } else {
    newRoot = updateVNode(
      newRoot,
      list.__ownerID,
      list._level,
      index,
      value,
      didAlter
    );
  }


以32位劃分存儲分區
const SHIFT = 5;
const SIZE = 1 << SHIFT;
function getTailOffset(size) {
  return size < SIZE ? 0 : ((size - 1) >>> 5) << 5;
}

2.is

is其實就是immutable中Map/List對象的deepDiff,而實際真正的diff過程就是hash與漫長的迭代diff。若是你對比的兩個immutable中,一個data被hash過,另外一個數據又是由其衍生出來的,那diff效率將是最高的

3.Hash算法的原理與優化

1.檢測本地weakMap/stringHashCache中是否存在已hash過當前對象/字符串。

一方面經過WeakMap的弱引用,讓這些做爲key的obj能夠被gc,另外一方面對於數據的hash過程只會是愈來愈快

2.對於immutable Data的特殊對象如何Hash?如DOMElement,非immutable Obj
對於DOMElement

首先檢測是否爲IE 低版本 IE對於每個DOM都賦予了惟一的node.uniqueID

function getIENodeHash(node) {
  if (node && node.nodeType > 0) {
    switch (node.nodeType) {
      case 1: // Element
        return node.uniqueID;
      case 9: // Document
        return node.documentElement && node.documentElement.uniqueID;
    }
  }
}

若爲非IE

手動維護一個遞增的hashWeakMap,Symbol私有化後放在prototype中

let UID_HASH_KEY = '__immutablehash__';
if (typeof Symbol === 'function') {
  UID_HASH_KEY = Symbol(UID_HASH_KEY);
}
hashed = ++objHashUID;
if (objHashUID & 0x40000000) {
    objHashUID = 0;
}
Object.defineProperty(obj, UID_HASH_KEY, {
  enumerable: false,
  configurable: false,
  writable: false,
  value: hashed,
});

對於非immutable Data(Map淺immutable后里的深層嵌套數據)

代碼同上,維護一個WeakMap,key是obj,Value是遞增的objHashUID

3.Hash衝突?merge KeyHash+ValueHash

對於純數組,immutable的hash方案是hash全部索引下的value而後進行疊加

對於object,immutable對每個object單元以Hash(key)+Hash(value)最後進行疊加

function hashCollection(collection) {
  if (collection.size === Infinity) {
    return 0;
  }
  const ordered = isOrdered(collection);
  const keyed = isKeyed(collection);
  let h = ordered ? 1 : 0;
  const size = collection.__iterate(
    keyed
      ? ordered
        ? (v, k) => {
            h = (31 * h + hashMerge(hash(v), hash(k))) | 0;
          }
        : (v, k) => {
            h = (h + hashMerge(hash(v), hash(k))) | 0;
          }
      : ordered
        ? v => {
            h = (31 * h + hash(v)) | 0;
          }
        : v => {
            h = (h + hash(v)) | 0;
          }
  );
  return murmurHashOfSize(size, h);
}

使用技巧

1.儘早提早hash的時間點,在一些ajax請求,launch加載的時候,這樣在進行長列表render的時候能夠很大程度上優化性能,同時安利一波biz-decorator,集成autobind,debounce,throttle,pureRender裝飾器

2.若是想用hash去作diff,要仔細考慮immutable是否Deep

Deep&Hash immutable時間長 初始hash時間長 diff速度快(與層次有關)

!Deep&Hash immutable時間短 初始hash時間短 diff速度快

!Deep&!Hash immutable時間短 無hash時間 diff速度快

結論:

Deep&Hash 耗時長,可是能夠給hashMap提供更多的hash樣本,前提是這個數據樣本會頻繁被用到

diff時無需對元數據衍生出來的數據hash化,並不會優化diff時間

//咱們對一個5MB的商品數據進行immutable

const Map = Immutable.Map(MockData) // 3.489013671875ms
Immutable.hash(Map)    // 1.677001953125ms

const fromJS = Immutable.fromJS(MockData) // 962.42724609375ms
Immutable.hash(fromJS)    // 306.51318359375ms

const Map2 = Map.setIn(['data','data',10,'state'],'5');

Immutable.is(Map2,Map) //3.2197265625ms

const fromJS2 = fromJS.setIn(['data','data',10,'state'],'5');

Immutable.is(fromJS2,fromJS) //10.624267578125ms

//相比以前fromJS的Immutable hash 時間成本節省了一個數量級
Immutable.hash(fromJS2); //16.772216796875ms

//diff時間上並無顯著的提高
Immutable.is(fromJS2,fromJS) //7.08203125ms

immutable缺點與解決方案

1.請求或存入LS時都須要轉成通用對象,可是仍然可使用JSON.stringify,也能夠toJS()

2.語法上基本兼容之前api(類ES6 Map/Set),可是寫法上有很大轉變(建議新項目或外部依賴較少的項目切immutable)

3.提供api較爲基礎,或達不到使用目的,能夠在原有基礎上擴展

4.基本經常使用類型多爲Map,List,可對immutable針對性的閹割,或者自行實行一套

相關文章
相關標籤/搜索