本文基于React^15.6.1
Redux^3.7.1
Immutable^4.0.0-rc.2
Immutable.js
Immutable Data
Immutable 實(shí)現(xiàn)的原理是 Persistent Data Structure(持久化數(shù)據(jù)結(jié)構(gòu))佩番,也就是使用舊數(shù)據(jù)創(chuàng)建新數(shù)據(jù)時(shí)众旗,要保證舊數(shù)據(jù)同時(shí)可用且不變。同時(shí)為了避免 deepCopy 把所有節(jié)點(diǎn)都復(fù)制一遍帶來(lái)的性能損耗答捕,Immutable 使用了 Structural Sharing(結(jié)構(gòu)共享)逝钥,即如果對(duì)象樹中一個(gè)節(jié)點(diǎn)發(fā)生變化,只修改這個(gè)節(jié)點(diǎn)和受它影響的父節(jié)點(diǎn)拱镐,其它節(jié)點(diǎn)則進(jìn)行共享艘款。
React 性能問(wèn)題
React的生命周期函數(shù)shuoldComponentUpdate
默認(rèn)是返回true
, 這樣的話每次state
或者props
改變的時(shí)候都會(huì)進(jìn)行render
,在render 的過(guò)程當(dāng)就是最消耗性能的過(guò)程.所以在生命周期函數(shù) shuoldComponentUpdate
中實(shí)現(xiàn)性能優(yōu)化.
-
可以使用
PrueComponent
,PrueComponent
是繼承至Component
PrueComponent
默認(rèn)進(jìn)行了一次shallowCompare淺比較,所謂淺比較就是只比較第一級(jí)的屬性是否相等相關(guān)源碼
node_modules/react/lib/ReactBaseClasses.js
function ReactPureComponent(props, context, updater) { // Duplicated from ReactComponent. this.props = props; this.context = context; this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } function ComponentDummy() {} ComponentDummy.prototype = ReactComponent.prototype; ReactPureComponent.prototype = new ComponentDummy(); ReactPureComponent.prototype.constructor = ReactPureComponent; // Avoid an extra prototype jump for these methods. //這里只是簡(jiǎn)單的將ReactComponent的原型復(fù)制到了ReactPureComponent的原型 _assign(ReactPureComponent.prototype, ReactComponent.prototype); ReactPureComponent.prototype.isPureReactComponent = true; module.exports = { Component: ReactComponent, PureComponent: ReactPureComponent };
node_modules/react-dom/lib/ReactCompositeComponent.js
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { *** ... var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = true; if (!this._pendingForceUpdate) { if (inst.shouldComponentUpdate) { if (process.env.NODE_ENV !== 'production') { shouldUpdate = measureLifeCyclePerf(function () { return inst.shouldComponentUpdate(nextProps, nextState, nextContext); }, this._debugID, 'shouldComponentUpdate'); } else { shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); } } else { if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); } } } *** ... },
再來(lái)看看
shallowEqual
node_modules/fbjs/lib/shallowEqual.js
var shallowEqual = require('fbjs/lib/shallowEqual'); *** // line 23 function is(x, y) { // SameValue algorithm if (x === y) { // Steps 1-5, 7-10 // Steps 6.b-6.e: +0 != -0 // Added the nonzero y check to make Flow happy, but it is redundant return x !== 0 || y !== 0 || 1 / x === 1 / y; } else { // Step 6.a: NaN == NaN return x !== x && y !== y; } } // line 41 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; } // 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; }
可以看到shallowEqual只對(duì)object的第一級(jí)屬性進(jìn)行比較
所以在基本數(shù)據(jù)類型之下我們可以直接繼承PureComponent
就能提升性能,比如一個(gè)最為普通的場(chǎng)景import React, { PureComponent } from 'react'; *** class MainPage extends PureComponent{ constructor(props,context){ super(props); this.props = props; this.state = { open: false }; this.toggleMenu = this.toggleMenu.bind(this); } toggleMenu() { this.setState({ open: !this.state.open }); } componentWillUnmount(){ console.log('componentWillUnmount-----mainpage') } render(){ let {match,location,localLang} = this.props; return ( <div> <AppBar title={null} iconElementRight={<UserHeader lang={localLang}/>} onLeftIconButtonTouchTap={this.toggleMenu} /> </div> ); } }
此處在點(diǎn)擊按鈕的時(shí)候直接調(diào)用
toggleMenu
執(zhí)行其中的setState
方法,'open'本來(lái)就是基本數(shù)據(jù)類型,即使不重寫shouldComponentUpdate,PureComponent的shouldComponentUpdate也完全能夠?qū)ζ溥M(jìn)行處理.進(jìn)一步我們可以看看state中處理引用數(shù)據(jù)類型的情況
最熟悉不過(guò)可能就是列表渲染,并更改列表狀態(tài)首先我們直接繼承
PureComponent
并不對(duì)數(shù)據(jù)進(jìn)行Immutable
的轉(zhuǎn)化class Item extends PureComponent{ constructor(props){ super(props); this.operatePositive = this.operatePositive.bind(this) this.operateNegative = this.operatePositive.bind(this) } static propTypes = { tile: PropTypes.object.isRequired, operate: PropTypes.func.isRequired } operatePositive(){ let id = this.props.tile._id; this.props.operate(true,id) } operateNegative(){ let id = this.props.tile._id; this.props.operate(false,id) } render(){ console.log('render item') let {tile} = this.props; return( <GridTile className={cx('grid-box')} > <img src={tile.images[0].thumb} /> { tile.operated ? null: <div className={cx('decide-box')}> <RaisedButton label="PASS" primary={true} onTouchTap = {this.operatePositive} icon={<FontIcon className="material-icons">done</FontIcon>} /> <RaisedButton label="BLOCK" onTouchTap = {this.operateNegative} icon={<FontIcon className="material-icons">block</FontIcon>} /> </div> } </GridTile> ) } } class Check extends PureComponent{ static propTypes = { lang: PropTypes.string.isRequired } constructor(props){ super(props) this.state = { list: [], count:{ blocked:0, passed:0, unusable:0 } } this.operate = this.operate.bind(this) } operate(usable,itemID){ console.log('----operate----') let list = this.state.list.map(item=>{ if(item.get('_id') == itemID){ return item.update('operated',false,(val)=>!val) } return item }) console.log(is(this.state.list,list)) this.setState({ list }) } getList(isInitial){ if(this.noMore) return false; let { lang } = this.props; let paramObj = { pageSize: 10 }; if(isInitial){ paramObj.properties = 'count'; } $get(`/api/multimedia/check/photos/${lang}`, paramObj) .then((res)=>{ let {data} = res; let obj = { list: data }; if(data.length < paramObj.pageSize){ this.noMore = true; } this.setState(obj) }) .catch(err=>{ console.log(err) }) } componentWillMount(){ this.getList('initial'); } componentWillUnmount(){ console.log('-----componentWillUnmount----') } render(){ let {list,count} = this.state; return( <GridList cellHeight={'auto'} cols={4} padding={1} className={cx('root')} > <Subheader> { list.length ? <div className={cx('count-table')}> <Chip>{count.blocked || 0} blocked</Chip><Chip>{count.passed || 0} passed</Chip><Chip>{count.unusable || 0} remaining</Chip> </div> : null } </Subheader> {list.map((tile,index) => ( <Item tile={tile} key={index} operate={this.operate}></Item> ))} </GridList> ); } }
初始化渲染并沒(méi)有什么問(wèn)題,直接執(zhí)行了10次
Item
的render
當(dāng)點(diǎn)擊操作按鈕的時(shí)候問(wèn)題來(lái)了,么有任何反應(yīng),經(jīng)過(guò)檢測(cè)原來(lái)是繼承了PureComponent
,在shouldComponentUpdate
返回了false,在看看shallowEqual
源碼,確實(shí)返回了false這樣的話我們先直接繼承
Component
,理論上任何一次setState
都會(huì)render 10次了class Item extends Component{ *** *** }
這次果然
render
了10次
-
使用
Immutable
進(jìn)行優(yōu)化此處需要注意,
Immutable.js
本身的入侵性還是比較強(qiáng),我們?cè)诟脑爝^(guò)程中需要注意與現(xiàn)有代碼的結(jié)合這里我們可以遵循幾個(gè)規(guī)則
- 在通過(guò)props傳遞的數(shù)據(jù),必須是
Immutable
的 - 在組件內(nèi)部的state可以酌情考慮是否需要
Immutable
- 基本數(shù)據(jù)類型(
Bool
,Number
,String
等)可以不進(jìn)行Immutable
處理 - 引用數(shù)據(jù)類型(
Array
,Object
)建議直接Immutable.fromJS
轉(zhuǎn)成Immutable
的對(duì)象
- 基本數(shù)據(jù)類型(
- ajax返回的數(shù)據(jù)我們可以根據(jù)第2點(diǎn)直接進(jìn)行轉(zhuǎn)化
- 在通過(guò)props傳遞的數(shù)據(jù),必須是
所以對(duì)代碼進(jìn)行如下改造
@pure
class Item extends PureComponent{
constructor(props){
super(props);
this.operatePositive = this.operatePositive.bind(this)
this.operateNegative = this.operatePositive.bind(this)
}
static propTypes = {
tile: PropTypes.object.isRequired,
operate: PropTypes.func.isRequired
}
operatePositive(){
let id = this.props.tile.get('_id');
this.props.operate(true,id)
}
operateNegative(){
let id = this.props.tile.get('_id');
this.props.operate(false,id)
}
render(){
console.log('render item')
let {tile} = this.props;
return(
<GridTile
className={cx('grid-box')}
>
<img src={tile.getIn(['images',0,'thumb'])} />
<div className={cx('decide-box')}>
<RaisedButton
label="PASS"
primary={true}
onTouchTap = {this.operatePositive}
icon={<FontIcon className="material-icons">done</FontIcon>}
/>
<RaisedButton
label="BLOCK"
onTouchTap = {this.operateNegative}
icon={<FontIcon className="material-icons">block</FontIcon>}
/>
</div>
</GridTile>
)
}
}
class Check extends PureComponent{
static propTypes = {
lang: PropTypes.string.isRequired
}
constructor(props){
super(props)
this.state = {
list: List([])
}
this.operate = this.operate.bind(this)
}
operate(usable,itemID){
let list = this.state.list.map(item=>{
if(item._id == itemID){
item.operated = true;
}
return item
})
console.log('----operate----')
this.setState({
list
})
}
getList(isInitial){
if(this.noMore) return false;
let { lang } = this.props;
let paramObj = {
pageSize: 10
};
$get(`/api/multimedia/check/photos/${lang}`, paramObj)
.then((res)=>{
let {data} = res;
//重點(diǎn)當(dāng)ajax數(shù)據(jù)返回之后引用數(shù)據(jù)類型直接轉(zhuǎn)化成Immutable的
let obj = {
list: fromJS(data)
};
this.setState(obj)
})
.catch(err=>{
console.log(err)
})
}
componentWillMount(){
this.getList('initial');
}
componentWillUnmount(){
console.log('-----componentWillUnmount----')
}
render(){
let {list,count} = this.state;
return(
<GridList
className={cx('root')}
>
{list.map((tile) => <Item key={tile.get('_id')} tile={tile} operate={this.operate}/>)}
</GridList>
);
}
}
當(dāng)點(diǎn)擊操作按鈕的之后,最終Item
的render調(diào)用如下
這里我們使用了一個(gè)裝飾器
@pure
具體邏輯可以根據(jù)項(xiàng)目數(shù)據(jù)結(jié)構(gòu)進(jìn)行實(shí)現(xiàn),如下代碼還有可以改進(jìn)空間
export const pure = (component)=>{
component.prototype.shouldComponentUpdate = function(nextProps,nextState){
let thisProps = this.props;
let thisState = this.state;
// console.log(thisState,nextState)
// if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
// Object.keys(thisState).length !== Object.keys(nextState).length) {
// return true;
// }
if(thisProps != nextProps){
for(const key in nextProps){
if(isImmutable(thisProps[key])){
if(!is(thisProps[key],nextProps[key])){
return true
}
}else{
if(thisProps[key]!= nextProps[key]){
return true;
}
}
}
}else if(thisState != nextState){
for(const key in nextState){
if(isImmutable(thisState[key])){
if(!is(thisState[key],nextState[key])){
return true
}
}else{
if(thisState[key]!= nextState[key]){
return true;
}
}
}
}
return false;
}
}
結(jié)合 redux
現(xiàn)在我們將剛才的組件代碼融入進(jìn)redux
的體系當(dāng)中
首先我們使用了redux-immutable,將初始化state進(jìn)行了Immutable的轉(zhuǎn)化
然后我們從組件觸發(fā)理一下思路,結(jié)合上文中Immutable對(duì)象
通過(guò)`redux`的`connect`關(guān)聯(lián)得到的數(shù)據(jù),最終是通過(guò)組件的`props`向下傳導(dǎo)的,所以`connect`所關(guān)聯(lián)的數(shù)據(jù)必須是`Immutable`的,這樣一來(lái)事情就好辦了,我們可以在`reducer`里面進(jìn)行統(tǒng)一處理,所有通過(guò)`redux`處理過(guò)的`state`必須是`Immutable`的,這就能保證所有的組件通過(guò)`connect`得到的屬性必然也是`Immutable`的
實(shí)現(xiàn)如下
//store.js
import { createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import Immutable from 'immutable';
import rootReducer from '../reducers';
let middleWareArr = [thunk];
//初始化store,注意需要建立Immutable的初始化state,詳細(xì)文檔可以查閱[redux-immutable](https://github.com/gajus/redux-immutable)
const initialState = Immutable.Map();
let store = createStore(rootReducer, initialState, applyMiddleware(...middleWareArr));
export default store;
// reducer.js
import { Map, List, fromJS, isImmutable } from 'immutable';
// 注意這里的`combineReducers` 是從`redux-immutable`引入的,可以查閱其詳細(xì)文檔,這里的主要作用是將導(dǎo)出的reducers轉(zhuǎn)化成Immutable的
import { combineReducers } from 'redux-immutable';
import * as ActionTypes from '~actions/user';
let authorState = Map({
checked: false
})
let author = (state = authorState, action)=>{
switch(action.type){
case ActionTypes.CHANGE_AUTHOR_STATUS:
state = state.set('checked',true)
break;
}
return state;
}
export default combineReducers({
author
});
//app.js
const mapStateToProps = (state)=>{
//這里傳入的state是`Immutable`的對(duì)象,等待傳入組件的`props`直接獲取即可
let author = state.getIn(['User','author'])
return {
author
}
}
@connect(mapStateToProps)
@prue
class AuthorApp extends PureComponent{
static propTypes = {
dispatch: PropTypes.func.isRequired,
author: PropTypes.object.isRequired
}
render(){
let { author } = this.props;
//author.getIn('checked') 這里是獲得真正需要的js屬性了
return (
author.getIn('checked') ? <App /> :
<MuiThemeProvider muiTheme={NDbaseTheme}>
<RefreshIndicator
className="page-indicator"
size={60}
left={0}
top={0}
status="loading"
/>
</MuiThemeProvider>
);
}
}
export default AuthorApp;
至此我們已經(jīng)將Immutable和redux進(jìn)行了結(jié)合.
總結(jié)如下
- 通過(guò)redux-immutable將初始化的state和所有reducer暴露出的對(duì)象轉(zhuǎn)成Immutable對(duì)象
- 在所有容器組件與reducer的連接函數(shù)
connect
中對(duì)組件的props屬性進(jìn)行統(tǒng)一Immutable
的限定,保證組件內(nèi)部能直接訪問(wèn)Immutable
的props - 在reducer中對(duì)action傳入的數(shù)據(jù)進(jìn)行Immutable話,返回一個(gè)Immutable的state
這樣一來(lái),Immutable就和redux集成到了一起