參考鏈接:
- Redux中文文檔
- Redux 入門教程-阮一峰
- 看漫畫,學 Redux
- 在react-native中使用redux
- [React Native]Redux的基本使用方式
- Redux管理復雜應用數據邏輯
目錄
- 應用場景
-
使用的三原則
- 單一數據源
- 狀態是只讀的
- 通過純函數修改State
-
redux狀態管理的流程及相關概念
- store
- Action
- Action 創建函數(Action Creator)
- Reducer
-
redux如何與組件結合
- 具體示例1
- 具體示例2
應用場景
React設計理念之一為單向數據流,這從一方面方便了數據的管理。但是React本身只是view,并沒有提供完備的數據管理方案。隨著應用的不斷復雜化,如果用react構建前端應用的話,就要應對紛繁復雜的數據通信和管理,js需要維護更多的狀態(state),這些state可能包括用戶信息、緩存數據、全局設置狀態、被激活的路由、被選中的標簽、是否加載動效或者分頁器等等。
這時,Flux架構應運而生,Redux是其最優雅的實現,Redux是一個不依賴任何庫的框架,但是與react結合的最好,其中react-redux等開源組件便是把react與redux組合起來進行調用開發。
備注:
1.如果你不知道是否需要 Redux,那就是不需要它
2.只有遇到 React 實在解決不了的問題,你才需要 Redux
Redux使用場景:
- 某個組件的狀態,需要共享
- 某個狀態需要在任何地方都可以拿到
- 一個組件需要改變全局狀態
- 一個組件需要改變另一個組件的狀態
比如,論壇應用中的夜間設置、回到頂部、userInfo全局共享等場景。redux最終目的就是讓狀態(state)變化變得可預測.
使用的三原則
- 單一數據源
整個應用的state,存儲在唯一一個object中,同時也只有一個store用于存儲這個object.
- 狀態是只讀的
唯一能改變state的方法,就是觸發action操作。action是用來描述正在發生的事件的一個對象
- 通過純函數修改State
純函數的問題,也是來自于函數式編程思想,我們在中學時學的函數就是純函數,對于同一個輸入,必然有相同的輸出。這就保證了數據的可控性,這里的純函數就是reducer
redux狀態管理的流程及相關概念
- store
Store 就是保存數據的地方,保存著本程序所有的redux管理的數據,你可以把它看成一個容器。整個應用只能有一個 Store。(一個store是一個對象, reducer會改變store中的某些值)
Redux 提供createStore這個函數,用來生成 Store。
import { createStore } from 'redux';
const store = createStore(fn);
上面代碼中,createStore函數接受另一個函數作為參數,返回新生成的 Store 對象。這個fn就是reducer純函數,通常我們在開發中也會使用中間件,來優化架構,比如最常用的異步操作插件,redux-thunk,如果配合redux-thunk來創建store的話,代碼示例:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootReudcer';
let createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
let store = createStoreWithMiddleware(rootReducer);
redux-thunk的源碼及其簡單,如下:
// 判斷action是否是函數,如果是,繼續執行遞歸式的操作。所以在redux中的異步,只能出現在action中,而且還需要有中間件的支持。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
同步action與異步action最大的區別是:
同步只返回一個普通action對象。而異步操作中途會返回一個promise函數。當然在promise函數處理完畢后也會返回一個普通action對象。thunk中間件就是判斷如果返回的是函數,則不傳導給reducer,直到檢測到是普通action對象,才交由reducer處理。
Store 有以下職責:
- 提供 getState() 方法獲取 state;
- 提供 dispatch(action) 方法更新 state;
- 通過 subscribe(listener) 注冊監聽器;
- 通過 subscribe(listener) 返回的函數注銷監聽器。
一般情況下,我們只需要getState()和dispatch()方法即可,即可以解決絕大部分問題。
我們可以自定義中間件
比如我們自定義一個可以打印出當前的觸發的action以及出發后的state變化的中間件,代碼改動如下:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootReudcer';
let logger = store => next => action => {
if(typeof action === 'function') {
console.log('dispatching a function')
}else{
console.log('dispatching', action);
}
let result = next(action);
// getState() 可以拿到store的狀態, 也就是所有數據
console.log('next state', store.getState());
return result;
}
let middleWares = {
logger,
thunk
}
// ... 擴展運算符
let createStoreWithMiddleware = applyMiddleware(...middleWares)(createStore);
let store = createStoreWithMiddleware(rootReducer);
補充:我們自定義的中間件,也有對應的開源插件,redux-logger,人家的更厲害。
如果,app中涉及到登錄問題的時候,可以使用redux-persist第三方插件,這個第三方插件來將store對象存儲到本地,以及從本地恢復數據到store中,比如說保存登錄信息,下次打開應用可以直接跳過登錄界面,因為我們目前的應用屬于內嵌程序,不登陸的話也進不來,所以不用它。
- Action
Action 是一個對象,描述了觸發的動作,僅此而已。我們約定,action 內必須使用一個字符串類型的 type 字段來表示將要執行的動作。通常它長一下這個樣子:
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
除了 type 字段外,action 對象的結構完全由你自己決定,來看一個復雜點的:
{
type: 'SET_SCREEN_LAST_REFRESH_TIME',
screenId,
lastRefreshTime,
objectId
}
通常我們會添加一個新的模塊文件來存儲這些actions types,比如我們新建一個actionTypes.js來保存:
//主頁actions
export const FETCH_HOME_LIST = 'FETCH_HOME_LIST';
export const RECEIVE_HOME_LIST = 'RECEIVE_HOME_LIST';
//分類頁actions
export const FETCH_CLASS_LIST = 'FETCH_CLASS_LIST';
export const RECEIVE_CLASS_LIST = 'RECEIVE_CLASS_LIST';
//分類詳細頁actions
export const FETCH_CLASSDITAL_LIST = 'FETCH_CLASSDITAL_LIST';
export const RECEIVE_CLASSDITAL_LIST = 'RECEIVE_CLASSDITAL_LIST';
export const RESET_CLASSDITAL_STATE = 'RESET_CLASSDITAL_STATE';
// 設置頁actions
export const CHANGE_SET_SWITCH = 'CHANGE_SET_SWITCH';
export const CHANGE_SET_TEXT = 'CHANGE_SET_TEXT';
// 用戶信息
export const USER_INFO = 'USER_INFO';
引用的時候,可以:
import * as types from './actionTypes';
- Action 創建函數(Action Creator)
Action 創建函數 就是生成 action 的方法。“action” 和 “action 創建函數” 這兩個概念很容易混在一起,使用時最好注意區分。在 Redux 中的 action 創建函數只是簡單的返回一個 action。
import * as types from './actionTypes';
// 設置詳情頁內容文字主題
let changeText = (theme) => {
return {
type: types.CHANGE_SET_TEXT,
theme
}
}
// 函數changeText就是一個簡單的action creator。
完整的action文件(setAction.js)
import * as types from './actionTypes';
let setTitle = (value) => {
return (dispatch, getState) => {
dispatch(changeValue(value))
}
}
let setText = (text) => {
return dispatch => {
dispatch(changeText(text))
}
}
// 修改標題顏色主題
let changeValue = (titleTheme) => {
return {
type: types.CHANGE_SET_SWITCH,
titleTheme
}
}
// 設置詳情頁內容文字顏色
let changeText = (textColor) => {
return {
type: types.CHANGE_SET_TEXT,
textColor
}
}
export {
setText,
setTitle
};
可以看到上述setTitle、setText函數,返回的并不是一個action對象,而是返回了一個函數,這個默認redux是沒法處理的,這就需要使用中間件處理了,redux-thunk中間件用于處理返回函數的函數,上面也介紹了redux-thunk的使用基本方式。
- Reducer
Store 收到 Action 以后,必須給出一個新的 State,這樣 View 才會發生變化。這種 State 的計算過程就叫做 Reducer。
Reducer 是一個函數,它接受 Action 和當前 State 作為參數,返回一個新的 State。
函數簽名:
(previousState, action) => newState
Reducer必須保持絕對純凈,永遠不要在 reducer 里做這些操作:
- 修改傳入參數;
- 執行有副作用的操作,如 API 請求和路由跳轉;
- 調用非純函數,如 Date.now() 或 Math.random();
完整的Reducer方法,(setReducer.js):
import * as types from '../actions/actionTypes';
const initialState = {
titleTheme: false,
textColor: false
}
// 這里一個技巧是使用 ES6 參數默認值語法 來精簡代碼
let setReducer = (state = initialState, action) => {
switch(action.type){
case types.CHANGE_SET_SWITCH:
return Object.assign({}, state, {
titleTheme: action.titleTheme,
})
case types.CHANGE_SET_TEXT:
return Object.assign({}, state, {
textColor: action.textColor
})
default:
return state;
}
}
export default setReducer
注意:
- 不要修改 state。 使用 Object.assign() 新建了一個副本。不能這樣使用 Object.assign(state, {
titleTheme: action.titleTheme,
}),因為它會改變第一個參數的值。你必須把第一個參數設置為空對象。你也可以開啟對ES7提案對象展開運算符的支持, 從而使用 { ...state, ...newState } 達到相同的目的。 - 在 default 情況下返回舊的 state。遇到未知的 action 時,一定要返回舊的 state
關于拆分Reducer
這里只是舉例了一個簡單的功能的reducer,如果有不同的功能,需要設計很多reducer方法,注意每個 reducer 只負責管理全局 state 中它負責的一部分。每個 reducer 的 state 參數都不同,分別對應它管理的那部分 state 數據。
比如我們這個項目的reducer文件結構:
其中rootReducer.js就是一個根reducer文件,使用了Redux 的 combineReducers()
工具類來進行封裝整合。
/**
* rootReducer.js
* 根reducer
*/
import { combineReducers } from 'redux';
import Home from './homeReducer';
import Class from './classReducer';
import ClassDetial from './classDetialReducer';
import setReducer from './setReducer';
import userReducer from './userReducer';
export default rootReducer = combineReducers({
Home,
Class,
ClassDetial,
setReducer,
userReducer,
})
這樣根據這個根reducer,可以生成store,請看上文store的創建過程。
redux如何與組件結合
以上部分介紹了Redux 涉及的基本概念,下面介紹與組件交互的工作流程。
梳理一下Redux的工作流程:
1.首先,用戶發出 Action。
store.dispatch(action);
2.Store 自動調用 Reducer,并且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。
let nextState = todoApp(previousState, action);
3.state一旦有變化,Store就會調用監聽函數,組件可以感知state的變化,更新View。
let newState = store.getState();
component.setState(newState);
具體示例1:
設置頁面有個switch按鈕,可以全局設置標題欄的主題。
代碼拆分:
1.設置按鈕所在組件:
// SetContainer.js
import React from 'react';
import {connect} from 'react-redux';
import SetPage from '../pages/SetPage';
class SetContainer extends React.Component {
render() {
return (
<SetPage {...this.props} />
)
}
}
export default connect((state) => {
const { setReducer } = state;
return {
setReducer
}
})(SetContainer);
這是容器組件,將SetPage組件與redux結合起來,其中最重要的方法是connect,這個示例中是將setReducer作為屬性傳給SetPage組件,關于connect的詳解,請移步到connect()。
2.SetPage組件
import React, {
Component
} from 'react';
import {
StyleSheet,
Text,
Image,
ListView,
TouchableOpacity,
View,
Switch,
InteractionManager,
} from 'react-native';
import Common from '../common/common';
import Loading from '../common/Loading';
import HeaderView from '../common/HeaderView';
import {setText,setTitle} from '../actions/setAction';
export default class SetPage extends Component {
constructor(props){
super(props);
this.state = {
switchValue: false,
textValue: false
}
this.onValueChange = this.onValueChange.bind(this);
this.onTextChange = this.onTextChange.bind(this);
}
componentDidMount() {
// console.log(this.props)
}
onValueChange(bool) {
const { dispatch } = this.props;
this.setState({
switchValue: bool
})
dispatch(setTitle(bool));
}
onTextChange(bool) {
const { dispatch } = this.props;
this.setState({
textValue: bool
});
dispatch(setText(bool));
}
render() {
return (
<View>
<HeaderView
titleView= {'設置'}
/>
<View>
<View style={styles.itemContainer}>
<Text style={{fontSize: 16}}>全局設置標題主題</Text>
<Switch
onValueChange={this.onValueChange}
value={this.state.switchValue}
/>
</View>
<View style={styles.itemContainer}>
<Text style={{fontSize: 16}}>設置詳情頁文字主題</Text>
<Switch
onValueChange={this.onTextChange}
value={this.state.textValue}
/>
</View>
</View>
</View>
)
}
}
const styles = StyleSheet.create({
itemContainer:{
paddingLeft: 20,
paddingRight: 20,
height: 40,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
}
})
可以只看全局設置標題主題這個方法,設置詳情頁文字顏色和他同理。這里可以清晰的看到,用戶切換主題switch按鈕的時候,觸發的方法:
dispatch(setTitle(bool));
3.我們查看一下setTitle這個action的源碼:
// setAction.js
import * as types from './actionTypes';
let setTitle = (value) => {
return (dispatch, getState) => {
dispatch(changeValue(value))
}
}
let setText = (text) => {
return dispatch => {
dispatch(changeText(text))
}
}
// 修改標題主題
let changeValue = (titleTheme) => {
return {
type: types.CHANGE_SET_SWITCH,
// 這里將titleTheme狀態返回
titleTheme
}
}
// 設置詳情頁內容文字主題
let changeText = (textColor) => {
return {
type: types.CHANGE_SET_TEXT,
textColor
}
}
export {
setText,
setTitle
};
4.action只是負責發送事件,并不會返回一個新的state供頁面組件調用,它是在reducer中返回的:
// setReducer.js
import * as types from '../actions/actionTypes';
const initialState = {
titleTheme: false,
textColor: false
}
let setReducer = (state = initialState, action) => {
switch(action.type){
case types.CHANGE_SET_SWITCH:
return Object.assign({}, state, {
titleTheme: action.titleTheme,
})
case types.CHANGE_SET_TEXT:
return Object.assign({}, state, {
textColor: action.textColor
})
default:
return state;
}
}
export default setReducer
最簡單的reducer,就是根據初始值和action對象,返回一個新的state,提供給store,這樣,頁面里可以從store中獲取到這些全局的state,用于更新組件。
我們只是寫了怎樣發送action和接收action發出newState的,下面來看這個標題組件是怎樣和redux結合的。
5.HeaderView組件
/**
* Created by ljunb on 16/5/8.
* 導航欄標題
*/
import React from 'react';
import {
StyleSheet,
View,
Text,
Image,
TouchableOpacity,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import Common from '../common/common';
import {connect} from 'react-redux';
class HeaderView extends React.Component {
constructor(props){
super(props);
this.state = {
}
}
render() {
// 這里,在這里
const { titleTheme } = this.props.setReducer;
let NavigationBar = [];
// 左邊圖片按鈕
if (this.props.leftIcon != undefined) {
NavigationBar.push(
<TouchableOpacity
key={'leftIcon'}
activeOpacity={0.75}
style={styles.leftIcon}
onPress={this.props.leftIconAction}
>
<Icon color="black" size={30} name={this.props.leftIcon}/>
</TouchableOpacity>
)
}
// 標題
if (this.props.title != undefined) {
NavigationBar.push(
<Text key={'title'} style={styles.title}>{this.props.title}</Text>
)
}
// 自定義標題View
if (this.props.titleView != undefined) {
let Component = this.props.titleView;
NavigationBar.push(
<Text key={'titleView'} style={[styles.titleView, {color: titleTheme ? '#FFF' : '#000'}]}>{this.props.titleView}</Text>
)
}
return (
<View style={[styles.navigationBarContainer, {backgroundColor: titleTheme ? 'blue' : '#fff'}]}>
{NavigationBar}
</View>
)
}
}
const styles = StyleSheet.create({
navigationBarContainer: {
marginTop: 20,
flexDirection: 'row',
height: 44,
justifyContent: 'center',
alignItems: 'center',
borderBottomColor: '#ccc',
borderBottomWidth: 0.5,
backgroundColor: 'white'
},
title: {
fontSize: 15,
marginLeft: 15,
},
titleView: {
fontSize: 15,
},
leftIcon: {
left: -Common.window.width/2+40,
},
})
export default connect((state) => {
const { setReducer } = state;
return {
setReducer
}
})(HeaderView);
這個組件同樣利用connect方法綁定了redux,變成了容器組件(container component)。
connect真的很關鍵,請詳細查看官方文檔,上面有鏈接。
其他不相關的內容忽略,核心代碼是:
// 拿到全局的state 當有變化的時候,會馬上修改
const { titleTheme } = this.props.setReducer;
具體示例2:
利用redux來請求數據、下拉刷新、上拉加載更多。
1.首先,封裝action。
import * as types from './actionTypes';
import Util from '../common/utils';
// action創建函數,此處是渲染首頁的各種圖片
export let home = (tag, offest, limit, isLoadMore, isRefreshing, isLoading) => {
let URL = 'http://api.huaban.com/fm/wallpaper/pins?limit=';
if (limit) URL += limit;
offest ? URL += '&max=' + offest : URL += '&max=';
tag ? URL += '&tag=' + encode_utf8(tag) : URL += '&tag='
return dispatch => {
// 分發事件 不修改狀態 action是 store 數據的唯一來源。
dispatch(feachHomeList(isLoadMore, isRefreshing, isLoading));
return Util.get(URL, (response) => {
// 請求數據成功后
dispatch(receiveHomeList(response.pins))
}, (error) => {
// 請求失敗
dispatch(receiveHomeList([]));
});
}
}
function encode_utf8(s) {
return encodeURIComponent(s);
}
// 我們約定,action 內必須使用一個字符串類型的 type 字段來表示將要執行的動作。
let feachHomeList = (isLoadMore, isRefreshing, isLoading) => {
return {
type: types.FETCH_HOME_LIST,
isLoadMore: isLoadMore,
isRefreshing: isRefreshing,
isLoading: isLoading,
}
}
let receiveHomeList = (homeList) => {
return {
type: types.RECEIVE_HOME_LIST,
homeList: homeList,
}
}
- feachHomeList表示正在請求數據的動作;
- receiveHomeList表示請求數據完后的動作;
- dispatch(feachHomeList(isLoadMore, isRefreshing, isLoading));表示分發請求數據的動作;
2.封裝reducer函數
import * as types from '../actions/actionTypes';
// 設置初始狀態
const initialState = {
HomeList: [],
isLoading: true,
isLoadMore: false,
isRefreshing: false,
};
let homeReducer = (state = initialState, action) => {
switch (action.type) {
case types.FETCH_HOME_LIST:
return Object.assign({}, state, {
isLoadMore: action.isLoadMore,
isRefreshing: action.isRefreshing,
isLoading: action.isLoading
})
case types.RECEIVE_HOME_LIST:
// 如果請求成功后,返回狀態給組件更新數據
return Object.assign({}, state, {
// 如果是正在加載更多,那么合并數據
HomeList: state.isLoadMore ? state.HomeList.concat(action.homeList) : action.homeList,
isRefreshing: false,
isLoading: false,
})
case types.RESET_STATE: // 清除數據
return Object.assign({},state,{
HomeList:[],
isLoading:true,
})
default:
return state;
}
}
export default homeReducer;
- 這里并沒有處理沒有更多數據的情況。
3.容器組件
import React from 'react';
import {connect} from 'react-redux';
import Home from '../pages/Home';
class HomeContainer extends React.Component {
render() {
return (
<Home {...this.props} />
)
}
}
export default connect((state) => {
const { Home } = state;
return {
Home
}
})(HomeContainer);
- 這里主要是利用connect函數將Home state綁定到Home組件中,并作為它的props;
4.UI組件
- 組件掛載請求數據
...
let limit = 21;
let offest = '';
let tag = '';
let isLoadMore = false;
let isRefreshing = false;
let isLoading = true;
...
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
const {dispatch} = this.props;
// 觸發action 請求數據
dispatch(home(tag, offest, limit, isLoadMore, isRefreshing, isLoading));
})
}
...
- 下拉刷新
// 下拉刷新
_onRefresh() {
if (isLoadMore) {
const {dispatch, Home} = this.props;
isLoadMore = false;
isRefreshing = true;
dispatch(home('', '', limit, isLoadMore, isRefreshing, isLoading));
}
}
- 上拉加載更多
// 上拉加載
_onEndReach() {
InteractionManager.runAfterInteractions(() => {
const {dispatch, Home} = this.props;
let homeList = Home.HomeList;
isLoadMore = true;
isLoading = false;
isRefreshing = false;
offest = homeList[homeList.length - 1].seq
dispatch(home(tag, offest, limit, isLoadMore, isRefreshing, isLoading));
})
}
- render方法
render() {
// 這里可以拿到Home狀態
const { Home,rowDate } = this.props;
tag = rowDate;
let homeList = Home.HomeList;
let titleName = '最新';
return (
<View>
<HeaderView
titleView= {titleName}
leftIcon={tag ? 'angle-left' : null}
/>
{Home.isLoading ? <Loading /> :
<ListView
dataSource={this.state.dataSource.cloneWithRows(homeList) }
renderRow={this._renderRow}
contentContainerStyle={styles.list}
enableEmptySections={true}
initialListSize= {10}
onScroll={this._onScroll}
onEndReached={this._onEndReach.bind(this) }
onEndReachedThreshold={10}
renderFooter={this._renderFooter.bind(this) }
style={styles.listView}
refreshControl={
<RefreshControl
refreshing={Home.isRefreshing}
onRefresh={this._onRefresh.bind(this) }
title="正在加載中……"
color="#ccc"
/>
}
/>
}
</View>
);
}
至此,一個簡單的Reducer程序完成了,我們稍微總結一下:
- 整個應用只有一個store,用來保存所有的狀態,視圖不需要自己維護狀態。
- 視圖通過connect函數綁定到store,當store狀態變化后,store會通知視圖刷新。
- 觸發一個action之后,會經過可能N個reducers處理,最后根reducer會將所有reducers處理之后的狀態合并,然后交給store,store再通知視圖刷新。
本文的源碼地址: 案例Demo