以Favorite組件為例分析 RN+Redux 狀態管理與數據流

無論使用React還是ReactNative,Redux總是繞不過的結(劫?解?)。近日在實現一個本地收藏組件的時候,淺顯但還算完整的使用了Redux來管理收藏的狀態與同步,因而有了本文(文末有demo視頻)。

0 準備

先上個參考文獻甩鍋。我講不清的,請查看參考文獻,還有個小Sample搭配

1 需求

Favorite組件,本文主角,其實就是一個收藏按鈕(圖1)。用戶點擊按鈕,按鈕變實心,收藏此篇文章,將這篇文章加入收藏列表(圖3),同時在所有顯示這篇文章的地方,自動同步收藏狀態。為簡單描述,省略server交互過程,我們假設收藏文章的信息都存在本地。

  • 這么lowbe的組件干嘛要用redux?: 因為有同步需求!我們需要做到一處收藏,處處‘亮星’,任意頁面收藏一篇文章,任何其他地方,即使是已經渲染好的父頁面,也同步此篇文章的收藏狀態——亮星或滅星。(例如:圖2中詳情頁是由圖1中的列表頁點擊進入的,如果用戶在詳情頁看完文章,點擊收藏,返回回來時,列表頁也會同步狀態)
    圖1-列表頁
圖2-文章詳情頁
圖3-收藏列表

2 實現

因為需求中明顯涉及到跨組件狀態的同步,所以用redux也就是很自然的了,react配合redux通常需要實現“四大金剛”:Action,Reducer,Container,Component,下面一一道來。

  • Action: 顧名思義,是一些動作的定義,因為redux這一類的狀態管理方式強調單向數據流與可追蹤,因此使用redux管理的數據,必須通過dispatch某一action來修改,這可以保證任意對于數據的修改都是可追蹤的,且一定是通過action這個入口進入的。
    如本例:定義兩個動作,ADD_FAVORITE和REMOVE_FAVORITE,當用戶點擊收藏按鈕,dispatch增加;在已收藏的按鈕上點擊,dispatch刪除。但是,請注意Action僅僅是定義,還未對數據真正進行修改,修改是下面那哥們的活兒。就好比皇帝餓了要吃肉,他(用戶)大喊一聲:我要吃肉,這只是先下了圣旨(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但從字面不好理解,但是其實可以將其理解為一個action的具體執行過程, reducer 就是一個純函數,接收舊的 state 和 action,返回新的 state:(previousState, action) => newState,就是這么簡單,一點兒都不恐怖對不對?請注意,針對Reducer,保持其純凈的計算屬性非常重要,所以請謹記永遠不要在 reducer 里做有副作用的或異步的一些操作,參考這兒
    • 新的state: 請務必注意是新的state,引用地址要變,而不要拿著一個引用地址在那兒狂賦值(我就做過),尤其針對子對象,子數組對象的元素增刪。原因主要是方便react監聽數據的變動,否則極有可能無法觸發組件的更新。
    • 調用api這一類怎么辦:多寫幾個action,發起api調用一個action;成功返回一個action,錯誤返回一個action,應該豁然開朗了吧?
import * as types from '../constants/ActionTypes';
import * as _ from 'lodash'

const initialState = {
    favoriteItems:[]//存儲用戶收藏的article列表,這一行只是設初值
};
//Reducer主體:很純粹的一個函數,接受老的state和action,返回新的state
export default function favorite(state = initialState, action) {
    switch (action.type) {
        case types.ADD_FAVORITE://收藏時對應的操作,將action帶過來的article加到列表中,仔細看此處的操作,返回的是《新的》state
            return Object.assign({}, state, {
                favoriteItems: insertItem(state.favoriteItems, action.article)
            });
        case types.REMOVE_FAVORITE://相對應的,刪除操作
            return Object.assign({}, state, {
                favoriteItems: removeItem(state.favoriteItems, action.article)
            });
        default:
            return state;
    }
}
//這兩個工具函數就是為了讓我們在每次數據更新時,返回的都是全新的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 :這兩個應該是獨立的部分,此處寫到一起是因為我在實現時代碼放到一起了,但是其職責完全不同:
    • container:容器組件,連接數據與展示組件的橋梁,主要做的就是把store的數據和action注入到展示組件中。
    • component:展示組件,這個不多講了,就是我們的普通組件,本例中這個組件內部就是畫了一個星星狀的按鈕。
兩種組件對比
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,//這是個特殊的props,來源于redux store,下面會看到,這個是自動注入的
    article: PropTypes.object
};
//展示組件定義
class FavoriteIcon extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            iconName: ''
        };
    }
    
    /*請注意,這兒針對組件渲染做了一點兒性能優化,因為本例中在任何收藏按鈕上點擊,都將修改
     FavoriteItems這個list,而只要這個list修改,就會觸發所有收藏按鈕的重新渲染判斷,這是不必要的,所以
     此處針對自己是否在新舊FavoriteItems做了一個異或,只有異或結果為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}//顯示實心的已收藏還是空心的未收藏
                backgroundColor="transparent"
                underlayColor="transparent"
                color={COLOR.HeaderText}
                activeOpacity={0.8}
                onPress={() => {
                    if (_.some(favoriteItems, {id: article.id})) {
                        favoriteActions.removeFavorite(article);//關鍵一步:我們在此處調用了注入進來的action,dispatch了一個remove favorite action
                        ToastUtil.showShort('Article removed from favorite');
                    } else {
                        favoriteActions.addFavorite(article);// 上同,dispatch add action
                        ToastUtil.showShort('Article marked as favorite');
                    }
                }}
            />
        )
    }
}
// 容器組件定義,可以看到,這個組件什么都沒做,只是引用了展示組件,并且把props穿進去,很好理解吧?
class Container extends React.Component {
    render() {
        return <FavoriteIcon {...this.props}/>
    }
}
//關鍵性操作,將redux store中的favoriteItems 注入到容器組件的props中
const mapStateToProps = (state) => {
    const {favoriteItems} = state.favorite;
    return {
        favoriteItems
    };
};
//關鍵性操作,將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生成容器組件,并且把相關的注入處理好,大功告成。
// 此時你就可以直接用這個容器組件了,就像用普通展示組件一樣,但是區別是,props里面會自動注入redux store中的相關data和action。
//只要redux store中data一變,props中相關數據就會變,從而自動觸發試圖更新。組件中的componentWillReceiveProps 也會觸發。
export default connect(mapStateToProps, mapDispatchToProps)(Container);
  • Finally,開心的用吧
<View>
  ...
  <FavoriteIcon article={article}/>// 記得傳入article對象哦
  ...
</View>

3 寫在最后

如果你有全部看完代碼實現邏輯,細心的你應該會發現,我有在展示組件里面做渲染性能優化,其實這是不得已而為之,因為整套組件的設計架構導致了每次的收藏都會導致store中favoriteitem列表的變化,而這個變化會導致所有icon的props變化,進而重渲染。此處用shouldComponentUpdate做過濾雖然避免了vitual dom比較的開銷,但是這個函數本身也有計算開銷,而且,virtual dom diff過程和此方法的執行開銷孰大孰小可能也要打個問號。在此我能想到的一個優化方式是將user對于一個article的收藏狀態臨時存于article,借助article的更新來refresh任意位置的收藏狀態。當然這需要做更多的操作,比如每次網絡獲取articlelist之后,都需要與本地favoriteList做merge,給已經收藏的文章打一個標記。所以,這是一個折中的過程,如果同時渲染的favorite icon數量不多,其實本文實現方式足夠了,也歡迎大家在評論區就優化方法留言討論 :)

另外,細心地你應該還會發現一個問題,favoriteItems沒有持久化?用戶關閉軟件再進來豈不是就沒了?沒錯,這個地方是需要持久化的,best practice自然是持久化到server,但是此處我們只持久化到了phone本地存儲,借助的是redux-persist,傻瓜式替我們做這一步,大概代碼如下:

const middlewares = [];
middlewares.push(...);//你的其他中間件
export default function configureStore() {
    const store = createStore(
        rootReducer,
        undefined,
        compose(
            applyMiddleware(...middlewares),
            autoRehydrate()//magic 一般的幫我們統統的持久化了
        )
    );
    store.close = () => store.dispatch(END);
    persistStore(store, {storage: AsyncStorage});//用rn提供的AsyncStore做save 引擎
    return store;
}

The End ,歡迎留言討論

f95f5d7455643e7543ae218bfae8b0bc.gif

原文鏈接:http://www.lxweimin.com/p/c925e84ec06a
作者: changchao 轉載請注明出處

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容