無論使用React還是ReactNative,Redux總是繞不過的結(jié)(劫腐巢?解?)玄括。近日在實(shí)現(xiàn)一個(gè)本地收藏組件的時(shí)候冯丙,淺顯但還算完整的使用了Redux來管理收藏的狀態(tài)與同步,因而有了本文(文末有demo視頻)遭京。
0 準(zhǔn)備
先上個(gè)參考文獻(xiàn)甩鍋胃惜。我講不清的,請(qǐng)查看參考文獻(xiàn)哪雕,還有個(gè)小Sample搭配
1 需求
Favorite組件船殉,本文主角,其實(shí)就是一個(gè)收藏按鈕(圖1)斯嚎。用戶點(diǎn)擊按鈕捺弦,按鈕變實(shí)心,收藏此篇文章孝扛,將這篇文章加入收藏列表(圖3),同時(shí)在所有顯示這篇文章的地方幽崩,自動(dòng)同步收藏狀態(tài)苦始。為簡(jiǎn)單描述,省略server交互過程慌申,我們假設(shè)收藏文章的信息都存在本地陌选。
-
這么lowbe的組件干嘛要用redux?: 因?yàn)橛型叫枨筇愀龋∥覀冃枰龅揭惶幨詹刈捎停幪帯列恰我忭?yè)面收藏一篇文章柒爵,任何其他地方役电,即使是已經(jīng)渲染好的父頁(yè)面,也同步此篇文章的收藏狀態(tài)——亮星或滅星棉胀。(例如:圖2中詳情頁(yè)是由圖1中的列表頁(yè)點(diǎn)擊進(jìn)入的法瑟,如果用戶在詳情頁(yè)看完文章冀膝,點(diǎn)擊收藏,返回回來時(shí)霎挟,列表頁(yè)也會(huì)同步狀態(tài))
2 實(shí)現(xiàn)
因?yàn)樾枨笾忻黠@涉及到跨組件狀態(tài)的同步窝剖,所以用redux也就是很自然的了,react配合redux通常需要實(shí)現(xiàn)“四大金剛”:Action酥夭,Reducer赐纱,Container,Component熬北,下面一一道來疙描。
-
Action: 顧名思義,是一些動(dòng)作的定義蒜埋,因?yàn)閞edux這一類的狀態(tài)管理方式強(qiáng)調(diào)單向數(shù)據(jù)流與可追蹤淫痰,因此使用redux管理的數(shù)據(jù),必須通過dispatch某一action來修改,這可以保證任意對(duì)于數(shù)據(jù)的修改都是可追蹤的整份,且一定是通過action這個(gè)入口進(jìn)入的待错。
如本例:定義兩個(gè)動(dòng)作,ADD_FAVORITE和REMOVE_FAVORITE烈评,當(dāng)用戶點(diǎn)擊收藏按鈕火俄,dispatch增加;在已收藏的按鈕上點(diǎn)擊讲冠,dispatch刪除瓜客。但是,請(qǐng)注意Action僅僅是定義竿开,還未對(duì)數(shù)據(jù)真正進(jìn)行修改谱仪,修改是下面那哥們的活兒。就好比皇帝餓了要吃肉否彩,他(用戶)大喊一聲:我要吃肉疯攒,這只是先下了圣旨(action),但是后廚(reducer)還沒開始做呢列荔!
import * as types from '../constants/ActionTypes';
export function addFavorite(article) {
return {
type: types.ADD_FAVORITE,//常量定義文件中定義好的常量字符串
article//收藏的文章object敬尺,{id:123,title:'hello',....}
};
}
export function removeFavorite(article) {
return {
type: types.REMOVE_FAVORITE,
article
};
}
-
Reducer:reducer但從字面不好理解,但是其實(shí)可以將其理解為一個(gè)action的具體執(zhí)行過程, reducer 就是一個(gè)純函數(shù)贴浙,接收舊的 state 和 action砂吞,返回新的 state:
(previousState, action) => newState
,就是這么簡(jiǎn)單,一點(diǎn)兒都不恐怖對(duì)不對(duì)崎溃?請(qǐng)注意蜻直,針對(duì)Reducer,保持其純凈的計(jì)算屬性非常重要,所以請(qǐng)謹(jǐn)記永遠(yuǎn)不要在 reducer 里做有副作用的或異步的一些操作袭蝗,參考這兒唤殴。- 新的state: 請(qǐng)務(wù)必注意是新的state,引用地址要變到腥,而不要拿著一個(gè)引用地址在那兒狂賦值(我就做過)朵逝,尤其針對(duì)子對(duì)象,子數(shù)組對(duì)象的元素增刪乡范。原因主要是方便react監(jiān)聽數(shù)據(jù)的變動(dòng)配名,否則極有可能無法觸發(fā)組件的更新。
- 調(diào)用api這一類怎么辦:多寫幾個(gè)action晋辆,發(fā)起api調(diào)用一個(gè)action渠脉;成功返回一個(gè)action,錯(cuò)誤返回一個(gè)action瓶佳,應(yīng)該豁然開朗了吧芋膘?
import * as types from '../constants/ActionTypes';
import * as _ from 'lodash'
const initialState = {
favoriteItems:[]//存儲(chǔ)用戶收藏的article列表,這一行只是設(shè)初值
};
//Reducer主體:很純粹的一個(gè)函數(shù)霸饲,接受老的state和action为朋,返回新的state
export default function favorite(state = initialState, action) {
switch (action.type) {
case types.ADD_FAVORITE://收藏時(shí)對(duì)應(yīng)的操作,將action帶過來的article加到列表中厚脉,仔細(xì)看此處的操作习寸,返回的是《新的》state
return Object.assign({}, state, {
favoriteItems: insertItem(state.favoriteItems, action.article)
});
case types.REMOVE_FAVORITE://相對(duì)應(yīng)的,刪除操作
return Object.assign({}, state, {
favoriteItems: removeItem(state.favoriteItems, action.article)
});
default:
return state;
}
}
//這兩個(gè)工具函數(shù)就是為了讓我們?cè)诿看螖?shù)據(jù)更新時(shí)傻工,返回的都是全新的article列表
function insertItem(array, item) {
let newArray = array.slice();
newArray.splice(0, 0, item);
return newArray;
}
function removeItem(array, item) {
let newArray = array.slice();
_.remove(newArray,{id: item.id});
return newArray;
}
-
container & component :這兩個(gè)應(yīng)該是獨(dú)立的部分霞溪,此處寫到一起是因?yàn)槲以趯?shí)現(xiàn)時(shí)代碼放到一起了,但是其職責(zé)完全不同:
- container:容器組件中捆,連接數(shù)據(jù)與展示組件的橋梁鸯匹,主要做的就是把store的數(shù)據(jù)和action注入到展示組件中。
- component:展示組件泄伪,這個(gè)不多講了殴蓬,就是我們的普通組件,本例中這個(gè)組件內(nèi)部就是畫了一個(gè)星星狀的按鈕臂容。
import React, {PropTypes} from 'react';
import Icon from 'react-native-vector-icons/Ionicons';
import * as _ from 'lodash';
import ToastUtil from "../utils/ToastUtil";
import * as COLOR from "../constants/Colors";
import * as creaters from '../actions/favorite';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
//容器組件接受的props
const propTypes = {
clickedName: PropTypes.string,
unClickedName: PropTypes.string,
favoriteItems: PropTypes.array,//這是個(gè)特殊的props,來源于redux store根蟹,下面會(huì)看到脓杉,這個(gè)是自動(dòng)注入的
article: PropTypes.object
};
//展示組件定義
class FavoriteIcon extends React.Component {
constructor(props) {
super(props);
this.state = {
iconName: ''
};
}
/*請(qǐng)注意,這兒針對(duì)組件渲染做了一點(diǎn)兒性能優(yōu)化简逮,因?yàn)楸纠性谌魏问詹匕粹o上點(diǎn)擊球散,都將修改
FavoriteItems這個(gè)list,而只要這個(gè)list修改散庶,就會(huì)觸發(fā)所有收藏按鈕的重新渲染判斷蕉堰,這是不必要的凌净,所以
此處針對(duì)自己是否在新舊FavoriteItems做了一個(gè)異或,只有異或結(jié)果為TRUE屋讶,才表示需要update
*/
shouldComponentUpdate(nextProps, nextState) {
if (this.props.article !== nextProps.article){
return true;
}
return (_.some(nextProps.favoriteItems, {id: this.props.article.id}) ^
_.some(this.props.favoriteItems, {id: this.props.article.id}))
}
render() {
let {clickedName, unClickedName, favoriteItems, article, favoriteActions} = this.props;
return (
<Icon.Button
name={_.some(favoriteItems, {id: article.id}) ? clickedName : unClickedName}//顯示實(shí)心的已收藏還是空心的未收藏
backgroundColor="transparent"
underlayColor="transparent"
color={COLOR.HeaderText}
activeOpacity={0.8}
onPress={() => {
if (_.some(favoriteItems, {id: article.id})) {
favoriteActions.removeFavorite(article);//關(guān)鍵一步:我們?cè)诖颂幷{(diào)用了注入進(jìn)來的action冰寻,dispatch了一個(gè)remove favorite action
ToastUtil.showShort('Article removed from favorite');
} else {
favoriteActions.addFavorite(article);// 上同,dispatch add action
ToastUtil.showShort('Article marked as favorite');
}
}}
/>
)
}
}
// 容器組件定義皿渗,可以看到斩芭,這個(gè)組件什么都沒做,只是引用了展示組件乐疆,并且把props穿進(jìn)去划乖,很好理解吧?
class Container extends React.Component {
render() {
return <FavoriteIcon {...this.props}/>
}
}
//關(guān)鍵性操作挤土,將redux store中的favoriteItems 注入到容器組件的props中
const mapStateToProps = (state) => {
const {favoriteItems} = state.favorite;
return {
favoriteItems
};
};
//關(guān)鍵性操作琴庵,將redux store中操作favoriteitems的action注入到容器組件的props中
const mapDispatchToProps = (dispatch) => {
const favoriteActions = bindActionCreators(creaters, dispatch);
return {
favoriteActions
};
};
Container.propTypes = propTypes;
Container.defaultProps = {
clickedName: "ios-star",
unClickedName: "ios-star-outline"
};
//此處用react-redux的connect生成容器組件,并且把相關(guān)的注入處理好仰美,大功告成迷殿。
// 此時(shí)你就可以直接用這個(gè)容器組件了,就像用普通展示組件一樣筒占,但是區(qū)別是贪庙,props里面會(huì)自動(dòng)注入redux store中的相關(guān)data和action。
//只要redux store中data一變翰苫,props中相關(guān)數(shù)據(jù)就會(huì)變止邮,從而自動(dòng)觸發(fā)試圖更新。組件中的componentWillReceiveProps 也會(huì)觸發(fā)奏窑。
export default connect(mapStateToProps, mapDispatchToProps)(Container);
- Finally导披,開心的用吧
<View>
...
<FavoriteIcon article={article}/>// 記得傳入article對(duì)象哦
...
</View>
3 寫在最后
如果你有全部看完代碼實(shí)現(xiàn)邏輯,細(xì)心的你應(yīng)該會(huì)發(fā)現(xiàn)埃唯,我有在展示組件里面做渲染性能優(yōu)化撩匕,其實(shí)這是不得已而為之,因?yàn)檎捉M件的設(shè)計(jì)架構(gòu)導(dǎo)致了每次的收藏都會(huì)導(dǎo)致store中favoriteitem列表的變化墨叛,而這個(gè)變化會(huì)導(dǎo)致所有icon的props變化止毕,進(jìn)而重渲染。此處用shouldComponentUpdate做過濾雖然避免了vitual dom比較的開銷漠趁,但是這個(gè)函數(shù)本身也有計(jì)算開銷扁凛,而且,virtual dom diff過程和此方法的執(zhí)行開銷孰大孰小可能也要打個(gè)問號(hào)闯传。在此我能想到的一個(gè)優(yōu)化方式是將user對(duì)于一個(gè)article的收藏狀態(tài)臨時(shí)存于article谨朝,借助article的更新來refresh任意位置的收藏狀態(tài)。當(dāng)然這需要做更多的操作,比如每次網(wǎng)絡(luò)獲取articlelist之后字币,都需要與本地favoriteList做merge则披,給已經(jīng)收藏的文章打一個(gè)標(biāo)記。所以洗出,這是一個(gè)折中的過程士复,如果同時(shí)渲染的favorite icon數(shù)量不多,其實(shí)本文實(shí)現(xiàn)方式足夠了共苛,也歡迎大家在評(píng)論區(qū)就優(yōu)化方法留言討論 :)
另外判没,細(xì)心地你應(yīng)該還會(huì)發(fā)現(xiàn)一個(gè)問題,favoriteItems沒有持久化隅茎?用戶關(guān)閉軟件再進(jìn)來豈不是就沒了澄峰?沒錯(cuò),這個(gè)地方是需要持久化的辟犀,best practice自然是持久化到server俏竞,但是此處我們只持久化到了phone本地存儲(chǔ),借助的是redux-persist,傻瓜式替我們做這一步,大概代碼如下:
const middlewares = [];
middlewares.push(...);//你的其他中間件
export default function configureStore() {
const store = createStore(
rootReducer,
undefined,
compose(
applyMiddleware(...middlewares),
autoRehydrate()//magic 一般的幫我們統(tǒng)統(tǒng)的持久化了
)
);
store.close = () => store.dispatch(END);
persistStore(store, {storage: AsyncStorage});//用rn提供的AsyncStore做save 引擎
return store;
}
The End ,歡迎留言討論
原文鏈接:http://www.reibang.com/p/c925e84ec06a
作者: changchao 轉(zhuǎn)載請(qǐng)注明出處