爲啥要用immutable.js呢。絕不誇張的說。有了immutable.js(固然也有其餘實現庫)。。才能將react的性能發揮到極致!要是各位看官用過一段時間的react,而沒有用immutable那麼本文很是適合你。react
想象一下這種場景,一個父組建下面一大堆子組建。而後呢,這個父組建re-render。是否是下面的子組建都得跟着re-render。但是不少子組建裏面是冤枉的啊!!不少子組建的props 和 state 然而並無改變啊!!雖然virtual dom 的diff 算法很快。。可是性能也不是這麼浪費的啊!!算法
如下是父組件代碼。。負責輸入name 和 age 而後循環顯示name 和 ageredux
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而已api
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了數組
由於咱用的是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語法。上面這麼寫就和下面這麼寫同樣less
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的源代碼就一目瞭然dom
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的時候考慮兩種狀況
這樣就會引發一個bug,好比我修改detail.name,由於detail的引用沒有改,因此
props.detail ===nextProps.detail 仍是爲true。。
因此咱們爲了安全起見必須修改detail的引用,(redux的reducer就是這麼作的)
這種雖然沒有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 對象的任何修改或添加刪除操做都會返回一個新的 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
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 並無限定 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
|
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'