前言 本文主要介紹facebook推出的一個類庫immutable.js,以及如何將immutable.js集成到我們團隊現有的react+redux架構的移動端項目中。 本文較長(5000字左右),建議閱讀時間: 20 min 通過閱讀本文,你可以學習到: 什麼是immut ...
來自一位美團大牛的分享,相信可以幫助到你。
原文鏈接:https://juejin.im/post/5948985ea0bb9f006bed7472?utm_source=tuicool&utm_medium=referral
前言
本文主要介紹facebook推出的一個類庫immutable.js,以及如何將immutable.js集成到我們團隊現有的react+redux架構的移動端項目中。
本文較長(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引用類型的坑
先考慮如下兩個場景:
// 場景一
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對象都會創建一個新的不可變對象,而老的對象不會改變。
1.2 immutable.js介紹
現今,實現了immutable數據結構的js類庫有好多,immutable.js就是其中比較主流的類庫之一。
Immutable.js出自Facebook,是最流行的不可變數據結構的實現之一。它從頭開始實現了完全的持久化數據結構,通過使用像tries這樣的先進技術來實現結構共用。所有的更新操作都會返回新的值,但是在內部結構是共用的,來減少記憶體占用(和垃圾回收的失效)。
immutable.js主要有三大特性:
- Persistent data structure (持久化數據結構)
- structural sharing (結構共用)
- support lazy operation (惰性操作)
下麵我們來一一具體介紹下這三個特性:
1.2.1 Persistent data structure (持久化數據結構)
一般聽到持久化,在編程中第一反應應該是,數據存在某個地方,需要用到的時候就能從這個地方拿出來直接使用
但這裡說的持久化是另一個意思,用來描述一種數據結構,一般函數式編程中非常常見,指一個數據,在被修改時,仍然能夠保持修改前的狀態,從本質來說,這種數據類型就是不可變類型,也就是immutable
immutable.js提供了十餘種不可變的類型(List,Map,Set,Seq,Collection,Range等)
到這,有些同學可能會覺得,這和之前講的拷貝有什麼區別,也是每次都創建一個新對象,開銷一樣很大。ok,那接下來第二個特性會為你揭開疑惑。
1.2.2 structural sharing (結構共用)
(圖片來自網路)immutable使用先進的tries(字典樹)技術實現結構共用來解決性能問題,當我們對一個Immutable對象進行操作的時候,ImmutableJS會只clone該節點以及它的祖先節點,其他保持不變,這樣可以共用相同的部分,大大提高性能。
這邊岔開介紹一下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,不建議使用,得不償失。