為啥要用immutable.js呢。毫不誇張的說。有了immutable.js(當然也有其他實現庫)。。才能將react的性能發揮到極致!要是各位看官用過一段時間的react,而沒有用immutable那么本文非常適合你。
1,對於react的來說,如果父組建有多個子組建
想象一下這種場景,一個父組建下面一大堆子組建。然后呢,這個父組建re-render。是不是下面的子組建都得跟着re-render。可是很多子組建里面是冤枉的啊!!很多子組建的props 和 state 然而並沒有改變啊!!雖然virtual dom 的diff 算法很快。。但是性能也不是這么浪費的啊!!
以下是父組件代碼。。負責輸入name 和 age 然后循環顯示name 和 age
1 export default class extends Component{ 2 constructor(props){ 3 super(props); 4 this.state = { 5 name:"", 6 age :"", 7 persons:[] 8 } 9 } 10 11 render(){ 12 const {name,age,persons} = this.state 13 return ( 14 <div> 15 <span>姓名:<input value={name} name="name" onChange={this._handleChange.bind(this)}></input> 16 <span>年齡:</span><input value={age} name="age" onChange={this._handleChange.bind(this)}></input> 17 <input type="button" onClick={this._handleClick.bind(this)} value="確認"></input> 18 {persons.map((person,index)=>( 19 <Person key={index} name={person.name} age={person.age}></Person> 20 ))} 21 </div> 22 ) 23 24 } 25 _handleChange(event){ 26 this.setState({[event.target.name]:event.target.value}) 27 } 28 _handleClick(){ 29 const {name,age} = this.state 30 this.setState({ 31 name:"", 32 age :"", 33 persons:this.state.persons.concat([{name:name,age:age}]) 34 }) 35 36 } 37 38 }
以下是子組建代碼單純的顯示name和age而已
1 class Person extends Component { 2 componentWillReceiveProps(newProps){ 3 console.log(`我新的props的name是${newProps.name},age是${newProps.age}。我以前的props的name是${this.props.name},age是${this.props.age}是我要re-render了`); 4 } 5 render() { 6 const {name,age} = this.props; 7 8 return ( 9 <div> 10 <span>姓名:</span> 11 <span>{name}</span> 12 <span> age:</span> 13 <span>{age}</span> 14 </div> 15 ) 16 } 17 }
這樣看得出來了吧 每次添加人的時候就會導致子組件re-render了
2,PureRenderMixin
因為咱用的是es2015的 Component,所以已經不支持mixin了。。所以在這里我們用[Pure render decorator][5]代替PureRenderMixin,那么代碼如下
1 import pureRender from "pure-render-decorator" 2 ... 3 4 @pureRender 5 class Person extends Component { 6 render() { 7 console.log("我re-render了"); 8 const {name,age} = this.props; 9 10 return ( 11 <div> 12 <span>姓名:</span> 13 <span>{name}</span> 14 <span> age:</span> 15 <span>{age}</span> 16 </div> 17 ) 18 } 19 }
果然可以做到pure render。。在必須render 的時候才render
是es7的Decorators語法。上面這么寫就和下面這么寫一樣
1 class PersonOrigin extends Component { 2 render() { 3 console.log("我re-render了"); 4 const {name,age} = this.props; 5 6 return ( 7 <div> 8 <span>姓名:</span> 9 <span>{name}</span> 10 <span> age:</span> 11 <span>{age}</span> 12 </div> 13 ) 14 } 15 } 16 const Person = pureRender(PersonOrigin)
pureRender其實就是一個函數,接受一個Component。把這個Component搞一搞,返回一個Component
看他pureRender的源代碼就一目了然
1 function shouldComponentUpdate(nextProps, nextState) { 2 return shallowCompare(this, nextProps, nextState); 3 } 4 5 function pureRende(component) { 6 component.prototype.shouldComponentUpdate = shouldComponentUpdate; 7 return component; 8 } 9 module.exports = pureRender;
pureRender很簡單,就是把傳進來的component的shouldComponentUpdate給重寫掉了,原來的shouldComponentUpdate,無論怎樣都是return ture,現在不了,我要用shallowCompare比一比,shallowCompare代碼及其簡單,如下
1 function shallowCompare(instance, nextProps, nextState) { 2 return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState); 3 }
一目了然。分別拿現在props&state和要傳進來的props&state,用shallowEqual比一比,要是props&state都一樣的話,就return false,是不是感覺很完美?不。。這才剛剛開始,問題就出在shallowEqual上了
很多時候,父組件向子組件傳props的時候,可能會傳一個復雜類型,比如我們改下。
1 render() { 2 const {name,age,persons} = this.state 3 return ( 4 <div> 5 ...省略..... 6 {persons.map((person,index)=>( 7 <Person key={index} detail={person}></Person> 8 ))} 9 </div> 10 ) 11 }
person是一個復雜類型。。這就埋下了隱患,,在演示隱患前,我們先說說shallowEqual,是個什么東西,shallowEqual其實只比較props的第一層子屬性是不是相同,就像上述代碼,props 是如下
{
detail:{
name:"123",
age:"123"}
}
他只會比較props.detail ===nextProps.detail
那么問題來了,上代碼
如果我想修改detail的時候考慮兩種情況
情況一,我修改detail的內容,而不改detail的引用
這樣就會引起一個bug,比如我修改detail.name,因為detail的引用沒有改,所以
props.detail ===nextProps.detail 還是為true。。
所以我們為了安全起見必須修改detail的引用,(redux的reducer就是這么做的)
情況二,我修改detail的引用
這種雖然沒有bug,但是容易誤殺,比如如果我新舊兩個detail的內容是一樣的,豈不是還要,render。。所以還是不完美,,你可能會說用 深比較就好了,,但是 深比較及其消耗性能,要用遞歸保證每個子元素一樣.
有人說 Immutable 可以給 React 應用帶來數十倍的提升,也有人說 Immutable 的引入是近期 JavaScript 中偉大的發明,因為同期 React 太火,它的光芒被掩蓋了。這些至少說明 Immutable 是很有價值的,下面我們來一探究竟。
JavaScript 中的對象一般是可變的(Mutable),因為使用了引用賦值,新的對象簡單的引用了原始對象,改變新的對象將影響到原始對象。如 foo={a: 1}; bar=foo; bar.a=2
你會發現此時 foo.a
也被改成了 2
。雖然這樣做可以節約內存,但當應用復雜后,這就造成了非常大的隱患,Mutable 帶來的優點變得得不償失。為了解決這個問題,一般的做法是使用 shallowCopy(淺拷貝)或 deepCopy(深拷貝)來避免被修改,但這樣做造成了 CPU 和內存的浪費。
什么是 Immutable Data
Immutable Data 就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。Immutable 實現的原理是 Persistent Data Structure(持久化數據結構),也就是使用舊數據創建新數據時,要保證舊數據同時可用且不變。同時為了避免 deepCopy 把所有節點都復制一遍帶來的性能損耗,Immutable 使用了 Structural Sharing(結構共享),即如果對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。請看下面動畫:
1 // 原來的寫法 2 let foo = {a: {b: 1}}; 3 let bar = foo; 4 bar.a.b = 2; 5 console.log(foo.a.b); // 打印 2 6 console.log(foo === bar); // 打印 true 7 8 // 使用 immutable.js 后 9 import Immutable from 'immutable'; 10 foo = Immutable.fromJS({a: {b: 1}}); 11 bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 賦值 12 console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 1 13 console.log(foo === bar); // 打印 false 14 15 // 使用 seamless-immutable.js 后 16 import SImmutable from 'seamless-immutable'; 17 foo = SImmutable({a: {b: 1}}) 18 bar = foo.merge({a: { b: 2}}) // 使用 merge 賦值 19 console.log(foo.a.b); // 像原生 Object 一樣取值,打印 1 20 console.log(foo === bar); // 打印 false
1 function touchAndLog(touchFn) { 2 let data = { key: 'value' }; 3 touchFn(data); 4 console.log(data.key); // 猜猜會打印什么? 5 }
在不查看 touchFn
的代碼的情況下,因為不確定它對 data
做了什么,你是不可能知道會打印什么(這不是廢話嗎)。但如果 data
是 Immutable 的呢,你可以很肯定的知道打印的是 value
。
1 import { Map} from 'immutable'; 2 let a = Map({ 3 select: 'users', 4 filter: Map({ name: 'Cam' }) 5 }) 6 let b = a.set('select', 'people'); 7 8 a === b; // false 9 a.get('filter') === b.get('filter'); // true
上面 a 和 b 共享了沒有變化的 filter
節點。
Immutable 中的 Map 和 List 雖對應原生 Object 和 Array,但操作非常不同,比如你要用 map.get('key')
而不是 map.key
,array.get(0)
而不是 array[0]
。另外 Immutable 每次修改都會返回新對象,也很容易忘記賦值。
兩個 immutable 對象可以使用 ===
來比較,這樣是直接比較內存地址,性能最好。但即使兩個對象的值是一樣的,也會返回 false
:
1 let map1 = Immutable.Map({a:1, b:1, c:1}); 2 let map2 = Immutable.Map({a:1, b:1, c:1}); 3 map1 === map2; // false
Immutable.is(map1, map2); // true
Immutable.is
比較的是兩個對象的 hashCode
或 valueOf
(對於 JavaScript 對象)。由於 immutable 內部使用了 Trie 數據結構來存儲,只要兩個對象的 hashCode
相等,值就是一樣的。這樣的算法避免了深度遍歷比較,性能非常好。
后面會使用 Immutable.is
來減少 React 重復渲染,提高性能。
由於 Immutable 數據一般嵌套非常深,為了便於訪問深層數據,Cursor 提供了可以直接訪問這個深層數據的引用。
1 import Immutable from 'immutable'; 2 import Cursor from 'immutable/contrib/cursor'; 3 4 let data = Immutable.fromJS({ a: { b: { c: 1 } } }); 5 // 讓 cursor 指向 { c: 1 } 6 let cursor = Cursor.from(data, ['a', 'b'], newData => { 7 // 當 cursor 或其子 cursor 執行 update 時調用 8 console.log(newData); 9 }); 10 11 cursor.get('c'); // 1 12 cursor = cursor.update('c', x => x + 1); 13 cursor.get('c'); // 2
setState 的一個技巧
React 建議把 this.state
當作 Immutable 的,因此修改前需要做一個 deepCopy,顯得麻煩:
1 import '_' from 'lodash'; 2 3 const Component = React.createClass({ 4 getInitialState() { 5 return { 6 data: { times: 0 } 7 } 8 }, 9 handleAdd() { 10 let data = _.cloneDeep(this.state.data); 11 data.times = data.times + 1; 12 this.setState({ data: data }); 13 // 如果上面不做 cloneDeep,下面打印的結果會是已經加 1 后的值。 14 console.log(this.state.data.times); 15 } 16 }
使用 Immutable 后:
1 getInitialState() { 2 return { 3 data: Map({ times: 0 }) 4 } 5 }, 6 handleAdd() { 7 this.setState({ data: this.state.data.update('times', v => v + 1) }); 8 // 這時的 times 並不會改變 9 console.log(this.state.data.get('times')); 10 }
上面的 handleAdd
可以簡寫成:
1 handleAdd() { 2 this.setState(({data}) => ({ 3 data: data.update('times', v => v + 1) }) 4 }); 5 }
與 Flux 搭配使用
由於 Flux 並沒有限定 Store 中數據的類型,使用 Immutable 非常簡單。
現在是實現一個類似帶有添加和撤銷功能的 Store:
1 import { Map, OrderedMap } from 'immutable'; 2 let todos = OrderedMap(); 3 let history = []; // 普通數組,存放每次操作后產生的數據 4 5 let TodoStore = createStore({ 6 getAll() { return todos; } 7 }); 8 9 Dispatcher.register(action => { 10 if (action.actionType === 'create') { 11 let id = createGUID(); 12 history.push(todos); // 記錄當前操作前的數據,便於撤銷 13 todos = todos.set(id, Map({ 14 id: id, 15 complete: false, 16 text: action.text.trim() 17 })); 18 TodoStore.emitChange(); 19 } else if (action.actionType === 'undo') { 20 // 這里是撤銷功能實現, 21 // 只需從 history 數組中取前一次 todos 即可 22 if (history.length > 0) { 23 todos = history.pop(); 24 } 25 TodoStore.emitChange(); 26 } 27 });
Mutable 對象
在 JavaScript 中,對象是引用類型的數據,其優點在於頻繁的修改對象時都是在原對象的基礎上修改,並不需要重新創建,這樣可以有效的利用內存,不會造成內存空間的浪費,對象的這種特性可以稱之為 Mutable,中文的字面意思是「可變」。
對於 Mutable 的對象,其靈活多變的優點有時可能會成為其缺點,越是靈活多變的數據越是不好控制,對於一個復雜結構的對象來說,一不小心就在某個不經意間修改了數據,假如該對象又在多個作用域中用到,此時很難預見到數據是否改變以及何時改變的。
針對這種問題,常規的解決辦法可以通過將對象進行深拷貝的形式復制出一個新的對象,再在新對象上做修改的操作,這樣能確保數據的可控性,但是頻繁的復制會造成內存空間的大量浪費。
1
2
3
4
5
6
|
var
obj = {
/* 一個復雜結構的對象 */
};
// copy 出一個新的 obj2
// 但是 copy 操作會浪費內存空間
var
obj2 = deepClone(obj);
doSomething(obj2);
// 上面的函數之行完后,無論 obj2 是否變化,obj 肯定還是原來那個 obj
|
對於 Mutable 的對象的低效率操作主要體現在復制和比較上,而 Immutable 對象就是解決了這兩大低效的痛點。
普通的 Mutable 對象的深拷貝操作會將一整份數據都復制一遍,而 Immutable 對象在修改數據時並不會復制一整份數據,而是將變化的節點與未變化的節點的父子關系轉移到一個新節點上,類似於鏈表的結構。從 “復制” 的角度來看,做到了最小化的復制,未變化的部分都是共享的,Mutable 在復制的時候是 “全量”,而 Immutable 復制的是 “增量”,對於內存空間的使用率的比較高低立判。
並且基於每次修改一個 Immutable 對象都會創建一個新的 Immutable 對象的這種特性可以將數據的修改狀態保存成一組快照,這也是挺方便的。
再來說說比較操作。對於 Mutable 的對象,如果要比較兩個對象是否相等,必須遍歷對象的每個節點進行比較,對於結構復雜的對象來說,其效率肯定高不到哪去。對於 Immutable 對象,immutable.js 提供了直接判斷兩個 Immutable 對象的「值」是否相等的 API。
var
map1 = Immutable.Map({a:1, b:1, c:1});
var
map2 = Immutable.Map({a:1, b:1, c:1});
assert(map1 !== map2);
// 不同的 Immutable 實例,此時比較的是引用地址
assert(Immutable.is(map1, map2));
// map1 和 map2 的值相等,比較的是值
assert(map1.equals(map2));
// 與 Immutable.is 的作用一樣
var
mutableObj = {};
// 寫入數據
mutableObj.foo =
'bar'
;
// 讀取數據
console.log(mutableObj.foo);
var
immutableObj1 = Immutable.Map();
// 寫入數據
var
immutableObj2 = immutableObj1.set(
'foo'
,
'bar'
);
// 讀取數據
console.log(immutableObj2.get(
'foo'
));
// => 'bar'
var
immutableObj1 = Immutable.fromJS({
a: {
b:
'c'
},
d: [1, 2, 3]
});
// 讀取深層級的數據
console.log(immutableObj1.getIn([
'a'
,
'b'
]));
// => 'c'
console.log(immutableObj1.getIn([
'd'
, 1]));
// => 2
// 修改深層級的數據
var
immutableObj2 = immutableObj1.setIn([
'a'
,
'b'
],
'd'
);
console.log(immutableObj2.getIn([
'a'
,
'b'
]));
// => 'd'