以Favorite組件為例分析 RN+Redux 狀態(tài)管理與數(shù)據(jù)流

無論使用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))
    圖1-列表頁(yè)
圖2-文章詳情頁(yè)
圖3-收藏列表

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è)星星狀的按鈕臂容。
兩種組件對(duì)比
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 ,歡迎留言討論

f95f5d7455643e7543ae218bfae8b0bc.gif

原文鏈接:http://www.reibang.com/p/c925e84ec06a
作者: changchao 轉(zhuǎn)載請(qǐng)注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末声旺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子席楚,更是在濱河造成了極大的恐慌,老刑警劉巖税稼,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烦秩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡郎仆,警方通過查閱死者的電腦和手機(jī)只祠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扰肌,“玉大人抛寝,你說我怎么就攤上這事∈镄瘢” “怎么了盗舰?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)桂躏。 經(jīng)常有香客問我钻趋,道長(zhǎng),這世上最難降的妖魔是什么沼头? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任爷绘,我火速辦了婚禮,結(jié)果婚禮上进倍,老公的妹妹穿的比我還像新娘土至。我一直安慰自己,他們只是感情好猾昆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布陶因。 她就那樣靜靜地躺著,像睡著了一般垂蜗。 火紅的嫁衣襯著肌膚如雪楷扬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天贴见,我揣著相機(jī)與錄音烘苹,去河邊找鬼。 笑死片部,一個(gè)胖子當(dāng)著我的面吹牛镣衡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播档悠,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼廊鸥,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了辖所?” 一聲冷哼從身側(cè)響起惰说,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缘回,沒想到半個(gè)月后吆视,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡切诀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年揩环,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幅虑。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丰滑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出倒庵,到底是詐尸還是另有隱情褒墨,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布擎宝,位于F島的核電站郁妈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏绍申。R本人自食惡果不足惜噩咪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一顾彰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胃碾,春花似錦涨享、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至俄周,卻和暖如春吁讨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背峦朗。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工建丧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人波势。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓茶鹃,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親艰亮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闭翩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容