前言
本文主要介紹facebook推出的一個類庫immutable.js,以及如何將immutable.js集成到咱們團隊現有的react+redux架構的移動端項目中。javascript
本文較長(5000字左右),建議閱讀時間: 20 min前端
經過閱讀本文,你能夠學習到:
- 什麼是immutable.js,它的出現能解決什麼問題
- immutable.js的特性以及使用api
- 在一個redux+react的項目中,引入immutable.js能帶來什麼提高
- 如何集成immutable.js到react+redux中
- 集成先後的數據對比
- immutabe.js使用過程當中的一些注意點
目錄
- 一. immutable.js
- 1.1 原生js引用類型的坑
- 1.2 immutable.js介紹
- 1.2.1 Persistent data structure (持久化數據結構)
- 1.2.2 structural sharing (結構共享)
- 1.2.3 support lazy operation (惰性操做)
- 1.3 經常使用api介紹
- 1.4 immutable.js的優缺點
- 二. 在react+redux中集成immutable.js實踐
- 2.1 點餐H5項目引入immutable.js前的現狀
- 2.2 如何將immutableJS集成到一個react+redux項目中
- 2.2.1 明確集成方案,邊界界定
- 2.2.2 具體集成代碼實現方法
- 2.3 點餐H5項目優化先後對比
- 三. immutable.js使用過程當中的一些注意點
- 四. 總結
一. immutable.js
1.1 原生js引用類型的坑
先考慮以下兩個場景:java
// 場景一
var obj = {a:1, b:{c:2}};
func(obj);
console.log(obj) //輸出什麼??
// 場景二
var obj = ={a:1};
var obj2 = obj;
obj2.a = 2;
console.log(obj.a); // 2
console.log(obj2.a); // 2複製代碼
上面兩個場景相信你們平日裏開發過程當中很是常見,具體緣由相信你們也都知道了,這邊不展開細說了,一般這類問題的解決方案是經過淺拷貝或者深拷貝複製一個新對象,從而使得新對象與舊對象引用地址不一樣。
在js中,引用類型的數據,優勢在於頻繁的操做數據都是在原對象的基礎上修改,不會建立新對象,從而能夠有效的利用內存,不會浪費內存,這種特性稱爲mutable(可變),但偏偏它的優勢也是它的缺點,太過於靈活多變在複雜數據的場景下也形成了它的不可控性,假設一個對象在多處用到,在某一處不當心修改了數據,其餘地方很難預見到數據是如何改變的,針對這種問題的解決方法,通常就像剛纔的例子,會想複製一個新對象,再在新對象上作修改,這無疑會形成更多的性能問題以及內存浪費。
爲了解決這種問題,出現了immutable對象,每次修改immutable對象都會建立一個新的不可變對象,而老的對象不會改變。react
1.2 immutable.js介紹
現今,實現了immutable數據結構的js類庫有好多,immutable.js就是其中比較主流的類庫之一。git
Immutable.js出自Facebook,是最流行的不可變數據結構的實現之一。它從頭開始實現了徹底的持久化數據結構,經過使用像tries這樣的先進技術來實現結構共享。全部的更新操做都會返回新的值,可是在內部結構是共享的,來減小內存佔用(和垃圾回收的失效)。es6
immutable.js主要有三大特性:github
- Persistent data structure (持久化數據結構)
- structural sharing (結構共享)
- support lazy operation (惰性操做)
下面咱們來一一具體介紹下這三個特性:web
1.2.1 Persistent data structure (持久化數據結構)
通常聽到持久化,在編程中第一反應應該是,數據存在某個地方,須要用到的時候就能從這個地方拿出來直接使用
但這裏說的持久化是另外一個意思,用來描述一種數據結構,通常函數式編程中很是常見,指一個數據,在被修改時,仍然可以保持修改前的狀態,從本質來講,這種數據類型就是不可變類型,也就是immutable
immutable.js提供了十餘種不可變的類型(List,Map,Set,Seq,Collection,Range等)
到這,有些同窗可能會以爲,這和以前講的拷貝有什麼區別,也是每次都建立一個新對象,開銷同樣很大。ok,那接下來第二個特性會爲你揭開疑惑。ajax
1.2.2 structural sharing (結構共享)
(圖片來自網絡)immutable使用先進的tries(字典樹)技術實現結構共享來解決性能問題,當咱們對一個Immutable對象進行操做的時候,ImmutableJS會只clone該節點以及它的祖先節點,其餘保持不變,這樣能夠共享相同的部分,大大提升性能。chrome
這邊岔開介紹一下tries(字典樹),咱們來看一個例子
(圖片來自網絡)
圖1就是一個字典樹結構object對象,頂端是root節點,每一個子節點都有一個惟一標示(在immutable.js中就是hashcode)
假設咱們如今取data.in的值,根據標記i和n的路徑.能夠找到包含5的節點.,可知data.in=5, 徹底不須要遍歷整個對象
那麼,如今咱們要把data.tea從3修改爲14,怎麼作呢?
能夠看到圖2綠色部分,不須要去遍歷整棵樹,只要從root開始找就行
實際使用時,能夠建立一個新的引用,如圖3,data.tea建一個新的節點,其餘節點和老的對象共享,而老的對象仍是保持不變
因爲這個特性,比較兩個對象時,只要他們的hashcode是相同的,他們的值就是同樣的,這樣能夠避免深度遍歷
1.2.3 support lazy operation (惰性操做)
- 惰性操做 Seq
- 特徵1:Immutable (不可變)
- 特徵2:lazy(惰性,延遲)
這個特性很是的有趣,這裏的lazy指的是什麼?很難用語言來描述,咱們看一個demo,看完你就明白了
這段代碼的意思就是,數組先取奇數,而後再對基數進行平方操做,而後在console.log第2個數,一樣的代碼,用immutable的seq對象來實現,filter只執行了3次,但原生執行了8次。
其實原理就是,用seq建立的對象,其實代碼塊沒有被執行,只是被聲明瞭,代碼在get(1)的時候纔會實際被執行,取到index=1的數以後,後面的就不會再執行了,因此在filter時,第三次就取到了要的數,從4-8都不會再執行
想一想,若是在實際業務中,數據量很是大,如在咱們點餐業務中,商戶的菜單列表可能有幾百道菜,一個array的長度是幾百,要操做這樣一個array,若是應用惰性操做的特性,會節省很是多的性能
1.3 經常使用api介紹
//Map() 原生object轉Map對象 (只會轉換第一層,注意和fromJS區別)
immutable.Map({name:'danny', age:18})
//List() 原生array轉List對象 (只會轉換第一層,注意和fromJS區別)
immutable.List([1,2,3,4,5])
//fromJS() 原生js轉immutable對象 (深度轉換,會將內部嵌套的對象和數組所有轉成immutable)
immutable.fromJS([1,2,3,4,5]) //將原生array --> List
immutable.fromJS({name:'danny', age:18}) //將原生object --> Map
//toJS() immutable對象轉原生js (深度轉換,會將內部嵌套的Map和List所有轉換成原生js)
immutableData.toJS();
//查看List或者map大小
immutableData.size 或者 immutableData.count()
// is() 判斷兩個immutable對象是否相等
immutable.is(imA, imB);
//merge() 對象合併
var imA = immutable.fromJS({a:1,b:2});
var imA = immutable.fromJS({c:3});
var imC = imA.merge(imB);
console.log(imC.toJS()) //{a:1,b:2,c:3}
//增刪改查(全部操做都會返回新的值,不會修改原來值)
var immutableData = immutable.fromJS({
a:1,
b:2,
c:{
d:3
}
});
var data1 = immutableData.get('a') // data1 = 1
var data2 = immutableData.getIn(['c', 'd']) // data2 = 3 getIn用於深層結構訪問
var data3 = immutableData.set('a' , 2); // data3中的 a = 2
var data4 = immutableData.setIn(['c', 'd'], 4); //data4中的 d = 4
var data5 = immutableData.update('a',function(x){return x+4}) //data5中的 a = 5
var data6 = immutableData.updateIn(['c', 'd'],function(x){return x+4}) //data6中的 d = 7
var data7 = immutableData.delete('a') //data7中的 a 不存在
var data8 = immutableData.deleteIn(['c', 'd']) //data8中的 d 不存在複製代碼
上面只列舉了部分經常使用方法,具體查閱官網api:facebook.github.io/immutable-j…
immutablejs還有不少相似underscore語法糖,使用immutable.js以後徹底能夠在項目中去除lodash或者underscore之類的工具庫。
1.4 immutable.js的優缺點
優勢:
- 下降mutable帶來的複雜度
- 節省內存
- 歷史追溯性(時間旅行):時間旅行指的是,每時每刻的值都被保留了,想回退到哪一步只要簡單的將數據取出就行,想一下若是如今頁面有個撤銷的操做,撤銷前的數據被保留了,只須要取出就行,這個特性在redux或者flux中特別有用
- 擁抱函數式編程:immutable原本就是函數式編程的概念,純函數式編程的特色就是,只要輸入一致,輸出必然一致,相比於面向對象,這樣開發組件和調試更方便
缺點:
- 須要從新學習api
- 資源包大小增長(源碼5000行左右)
- 容易與原生對象混淆:因爲api與原生不一樣,混用的話容易出錯。
二. 在react+redux中集成immutable.js實踐
前面介紹了這麼多,實際上是想引出這塊重點,這章節會結合點評點餐團隊在實際項目中的實踐,給出使用immutable.js先後對react+redux項目的性能提高
2.1 點餐H5項目引入immutable.js前的現狀
目前項目使用react+redux,因爲項目的不斷迭代以及需求複雜度的提升,redux中維護的state結構日漸龐大,已經不是一個簡單的平鋪數據了,如菜單頁state已經會出現三四層的object以及array嵌套,咱們知道,JS中的object與array是引用類型,在不斷的操做過程當中,state通過屢次的action改變以後, 本來複雜state已經變得不可控,結果就是致使了一次state變化牽動了許多自身狀態沒有發生改動的component去re-render。以下圖
這裏推薦一下react的性能指標工具react-addons-perf
若是你沒有使用這個工具看以前,別人問你,圖中這個簡單的堂食/外帶的button的變化會引發哪些component去re-render,你可能會回答只有就餐方式這個component。
但當你真正使用react-addons-perf去查看以後你會發現,WTF??!一次操做居然致使了這麼多沒任何關係的component從新渲染了??
什麼緣由??
shouldComponentUpdate
shouldComponentUpdate (nextProps, nextState) {
return nextProps.id !== this.props.id;
};複製代碼
相信接觸過react開發的同窗都知道,react有個重要的性能優化的點就是shouldComponentUpdate,shouldComponentUpdate返回true代碼該組件要re-render,false則不從新渲染
那簡單的場景能夠直接使用==去判斷this.props和nextProps是否相等,但當props是一個複雜的結構時,==確定是沒用的
網上隨便查一下就會發現shallowCompare這個東西,咱們來試一下
使用shallowCompare的例子:
能夠看到,其實2個對象的count是不相等的,但shallowCompare返回的仍是true
緣由:
shallowCompare只是進行了對象的頂層節點比較,也就是淺比較,上圖中的props因爲結構比較複雜,在深層的對象中有count不同,因此這種狀況沒法經過shallowCompare處理。
shallowEqual源碼:
function shallowEqual(objA, objB) {
if (is(objA, objB)) {
return true;
}
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
//這裏只比較了對象A和B第一層是否相等,當對象過深時,沒法返回正確結果
// Test for A's keys different from B.
for (var i = 0; i < keysA.length; i++) {
if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
return false;
}
}
return true;
}複製代碼
這裏,咱們確定不可能每次比較都是用深比較,去遍歷全部的結構,這樣帶來的性能代價是巨大的,剛纔咱們說到immutable.js有個特性是引用比較(hashcode),這個特性就完美契合這邊的場景
2.2 如何將immutableJS集成到一個react+redux項目中
2.2.1 明確集成方案,邊界界定
首先,咱們有必要來劃分一下邊界,哪些數據須要使用不可變數據,哪些數據要使用原生js數據結構,哪些地方須要作互相轉換
- 在redux中,全局state必須是immutable的,這點毋庸置疑是咱們使用immutable來優化redux的核心
- 組件props是經過redux的connect從state中得到的,而且引入immutableJS的另外一個目的是減小組件shouldComponentUpdate中沒必要要渲染,shouldComponentUpdate中比對的是props,若是props是原生JS就失去了優化的意義
- 組件內部state若是須要提交到store的,必須是immutable,不然不強制
- view提交到action中的數據必須是immutable
- Action提交到reducer中的數據必須是immutable
- reducer中最終處理state必須是以immutable的形式處理並返回
- 與服務端ajax交互中返回的callback統一封裝,第一時間轉換成immutable數據
從上面這些點能夠看出,幾乎整個項目都是必須使用immutable的,只有在少數與外部依賴有交互的地方使用了原生js。
這麼作的目的其實就是爲了防止在大型項目中,原生js與immutable混用,致使coder本身都不清楚一個變量中存儲的究竟是什麼類型的數據。
那有人可能會以爲說,在一個全新項目中這樣是可行的,但在一個已有的成熟項目中,要將全部的變量所有改爲immutablejs,代碼的改動量與侵入性很是大,風險也高。那他們會想到,將reducer中的state用fromJS()改爲immutable進行state操做,而後再經過toJS()轉成原生js返回出來,這樣不就能夠即讓state變得可追溯,又不用去修改reducer之外的代碼,代價很是的小。
export default function indexReducer(state, action) {
switch (action.type) {
case RECEIVE_MENU:
state = immutable.fromJS(state); //轉成immutable
state = state.merge({a:1});
return state.toJS() //轉回原生js
}
}複製代碼
兩點問題:
- fromJS() 和 toJS() 是深層的互轉immutable對象和原生對象,性能開銷大,儘可能不要使用(見下一章節作了具體的對比)
- 組件中props和state仍是原生js,shouldComponentUpdate仍然沒法作利用immutablejs的優點作深度比較
2.2.2 具體集成代碼實現方法
redux-immutable
redux中,第一步確定利用combineReducers來合併reducer並初始化state,redux自帶的combineReducers只支持state是原生js形式的,因此這裏咱們須要使用redux-immutable提供的combineReducers來替換原來的方法
import {combineReducers} from 'redux-immutable';
import dish from './dish';
import menu from './menu';
import cart from './cart';
const rootReducer = combineReducers({
dish,
menu,
cart,
});
export default rootReducer;複製代碼
reducer中的initialState確定也須要初始化成immutable類型
const initialState = Immutable.Map({});
export default function menu(state = initialState, action) {
switch (action.type) {
case SET_ERROR:
return state.set('isError', true);
}
}複製代碼
state成爲了immutable類型,那相應的頁面其餘文件都須要作相應的寫法改變
//connect
function mapStateToProps(state) {
return {
menuList: state.getIn(['dish', 'list']), //使用get或者getIn來獲取state中的變量
CartList: state.getIn(['dish', 'cartList'])
}
}複製代碼
頁面中原來的原生js變量須要改形成immutable類型,不一一列舉了
服務端交互ajax封裝
前端代碼使用了immutable,但服務端下發的數據仍是json,因此須要統一在ajax處作封裝而且將服務端返回數據轉成immutable
//僞代碼
$.ajax({
type: 'get',
url: 'XXX',
dataType: 'json',
success(res){
res = immutable.fromJS(res || {});
callback && callback(res);
},
error(e) {
e = immutable.fromJS(e || {});
callback && callback(e);
},
});複製代碼
這樣的話,頁面中統一將ajax返回當作immutable類型來處理,不用擔憂混淆
shouldComponentUpdate
重中之重!以前已經介紹了不少爲何要用immutable來改造shouldComponentUpdate,這裏就很少說了,直接看怎麼改造
shouldComponentUpdate具體怎麼封裝有不少種辦法,咱們這裏選擇了封裝一層component的基類,在基類中去統一處理shouldComponentUpdate,組件中直接繼承基類的方式
//baseComponent.js component的基類方法
import React from 'react';
import {is} from 'immutable';
class BaseComponent extends React.Component {
constructor(props, context, updater) {
super(props, context, updater);
}
shouldComponentUpdate(nextProps, nextState) {
const thisProps = this.props || {};
const thisState = this.state || {};
nextState = nextState || {};
nextProps = nextProps || {};
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 (!is(thisState[key], nextState[key])) {
return true;
}
}
return false;
}
}
export default BaseComponent;複製代碼
組件中若是須要使用統一封裝的shouldComponentUpdate,則直接繼承基類
import BaseComponent from './BaseComponent';
class Menu extends BaseComponent {
constructor() {
super();
}
…………
}複製代碼
固然若是組件不想使用封裝的方法,那直接在該組件中重寫shouldComponentUpdate就好了
2.3 點餐H5項目優化先後對比
這邊只是截了幾張圖舉例
優化前搜索頁:
優化後:
優化前購物車頁:
優化後:
三. immutable.js使用過程當中的一些注意點
1.fromJS和toJS會深度轉換數據,隨之帶來的開銷較大,儘量避免使用,單層數據轉換使用Map()和List()
(作了個簡單的fromJS和Map性能對比,同等條件下,分別用兩種方法處理1000000條數據,能夠看到fromJS開銷是Map的4倍)
2.js是弱類型,但Map類型的key必須是string!(看下圖官網說明)
3.全部針對immutable變量的增刪改必須左邊有賦值,由於全部操做都不會改變原來的值,只是生成一個新的變量
//javascript
var arr = [1,2,3,4];
arr.push(5);
console.log(arr) //[1,2,3,4,5]
//immutable
var arr = immutable.fromJS([1,2,3,4])
//錯誤用法
arr.push(5);
console.log(arr) //[1,2,3,4]
//正確用法
arr = arr.push(5);
console.log(arr) //[1,2,3,4,5]複製代碼
4.引入immutablejs後,不該該再出現對象數組拷貝的代碼(以下舉例)
//es6對象複製
var state = Object.assign({}, state, {
key: value
});
//array複製
var newArr = [].concat([1,2,3])複製代碼
5. 獲取深層深套對象的值時不須要作每一層級的判空
//javascript
var obj = {a:1}
var res = obj.a.b.c //error
//immutable
var immutableData=immutable.fromJS({a:1})
var res = immutableData.getIn(['a', 'b', 'c']) //undefined複製代碼
6.immutable對象直接能夠轉JSON.stringify(),不須要顯式手動調用toJS()轉原生
7. 判斷對象是不是空能夠直接用size
8.調試過程當中要看一個immutable變量中真實的值,能夠chrome中加斷點,在console中使用.toJS()方法來查看
四. 總結
總的來講immutable.js的出現解決了許多原生js的痛點,而且自身對性能方面作了許多的優化處理,並且immuable.js做爲和react同期推出的一個產品,完美的契合了react+redux的state流處理,redux的宗旨就是單一數據流,可追溯,這兩點偏偏是immutable.js的優點,天然水到渠成,何樂而不爲。 固然也不是全部使用react+redux的場景都須要使用immutable.js,建議知足項目足夠大,state結構足夠複雜的原則,小項目能夠手動處理shouldComponentUpdate,不建議使用,得不償失。