前言
React認為每個組件都是一個有限狀態機,狀態與UI是一一對應的。我們只需管理好APP的state就能控制UI的顯示,我們可以在每個component類中來通過this.state
和this.setState
來管理組件的state,但是如果APP交互比較多比較復雜,或者說該組件的某一狀態需要和其他組件共享的話,這種方式就有點復雜了。
有沒有一種能統一管理APP狀態的框架呢,這時候Redux就應用而生了,它是一個用于統一管理APP 所有的state的一個的js框架,它不建議我們在component中直接操作state,而是交給redux的store中進行處理。而react-redux又是redux的作者為react專門做的一個封裝,適用于react的狀態管理容器。
那么我們的項目到底需不需要redux呢,從組件角度看,如果你的應用有以下場景,可以考慮使用 Redux,否則無腦強行使用反而麻煩。
- 某個組件的狀態,需要共享
- 某個狀態需要在任何地方都可以拿到
- 一個組件需要改變全局狀態
- 一個組件需要改變另一個組件的狀態
redux的設計思想
- Web 應用是一個狀態機,視圖與狀態是一一對應的。
- 所有的狀態,保存在store對象里面,由其統一管理。
- 狀態在組件中是‘只讀’的,要交給redux處理
redux數據流程圖
有圖有真相,先來一張redux數據流圖,讓你有一個整體的把握
redux的相關概念
Action
Action 是把數據從應用(這里之所以不叫 view 是因為這些數據有可能是服務器響應,用戶輸入或其它非 view 的數據 )傳到 store 的有效載荷。它是 store 數據的唯一來源。一般來說你會通過 store.dispatch()
將 action 傳到 store。
按redux設計理念是不允許用戶直接操作組件的state,而是通過觸發動作(action)來更新state,用戶或后臺服務器可以通過store.dispatch(action)來向store觸發一個消息(消息至少一個標識該消息的字段type,還可以添加其他字段用于數據傳送),store會在內部根據消息的類型type去reducer中執行相應的處理,這個消息我們就叫他為Action,Action本質上是一個JavaScript對象。
實際編碼中一般會把整個應用的消息類型type統一放在一個文件ActionTypes.js中
export const ADD_TODO = 'ADD_TODO'
Action的結構如下,各個字段的key的名字可以隨意命名,但是類型的key一般都是type,數據類型最好為字符串
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
隨著程序越來越大,你會發現一個組件中的action太多太亂了,所以我們也會把action按業務分類放在各個指定的文件中,但是又有一個問題,若果每個action的字段都有五六個,我們在如下寫法豈不是太亂了
store.dispatch({
type: ADD_TODO,
text: 'Build my first Redux app'
})
于是乎我們就想起來可以將action對象封裝在函數中,這個函數返回一個action對象,這個返回一個action對象的函數我們就稱之為 action creator,如下所示
export let todo = ()=> {
return {
type: ADD_TODO,
text: 'Build my first Redux app'
}
}
我們直接store.dispatch(todo)就好了,看著是不是整潔多了啊
異步動作(async action)
事實上redux提供的dispatch方法只能接受純粹的action對象(即js中Object類型的對象),如下所示:
store.dispatch({
type: ADD_TODO,
text: 'Build my first Redux app'
})
這也是我們最常用的方式,這種方式屬于同步Action
,一旦觸發同步動作,redux就能同步的完成state的操作,但是我們在開發中會遇到這么一類acton,比如網絡請求,這是一個異步的過程,此時純粹的action對象已經滿足不了,我們需要采用異步Action
,異步Action本質上是一個函數,我們可以在此函數的回調中去調用dispatch來觸發action,而這一次的觸發是同步的,如下所示:
// 來看一下我們寫的第一個 thunk action 創建函數!
// 雖然內部操作不同,你可以像其它 action 創建函數 一樣使用它:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware 知道如何處理函數。
// 這里把 dispatch 方法通過參數的形式傳給函數,
// 以此來讓它自己也能 dispatch action。
return function (dispatch) {
// 首次 dispatch:更新應用的 state 來通知
// API 請求發起了。此時我們可以做一些操作,比如讓網絡指示器顯示出來
dispatch(requestPosts(subreddit))
// thunk middleware 調用的函數可以有返回值,
// 它會被當作 dispatch 方法的返回值傳遞
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用 catch,因為會捕獲
// 在 dispatch 和渲染中出現的任何錯誤,
// 導致 'Unexpected batch number' 錯誤。
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// 可以多次 dispatch,這里的dispatch是同步action
// 在這里我們可以把異步操作的結果 同步的觸發 給redux,同時隱藏網絡指示器(即改變網絡指示器的狀態)
dispatch(receivePosts(subreddit, json))
)
}
}
前面不是說dispatch只能接受js對象嗎,現在怎么可以接受一個函數了?其實這正是thunk middleware(中間件)的功能,他把原來的dispatch進行了包裝,進行了一系列的預處理,具體細節可以另行參考其他資源,這里不再詳述了。
reducer
它是一個純函數,要求有相同的輸入(參數)就一定會有相同的輸出,里面不能包含一些不確定的因素,比如new Date(),它會根據當前的state和action來進行邏輯處理返回一個新的state
參數一:當前的state對象
參數二:action對象
返回值:產生一個新的state對象
import { VisibilityFilters } from './actions'
//初始state
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
};
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
注意:reducer函數中一定不要去修改state,而是用Object.assign()函數生成一個新的state對象,如上所示
combineReducers:隨著應用變得復雜,把APP的所有狀態都放在一個reducer中處理會造成reducer函數非常龐大,因此需要對 reducer 函數 進行拆分,拆分后的每一個子reducer獨立負責管理 APP state 的一部分。combineReducers 輔助函數的作用是,把多個不同子reducer 函數合并成一個最終的根reducer ,最后將根 reducer 作為createStore的參數就可以創建store對象了。合并后的 reducer 可以調用各個子 reducer,并把它們的結果合并成一個 state 對象。state 對象的結構由傳入的多個 reducer 的 key 決定。
最終,state 對象的結構會是這樣的:
{
reducer1: ...
reducer2: ...
}
使用方法如下所示
import { combineReducers } from 'redux';
import Strolling from './strollingReducer';
import Foods from './foodsReducer';
import FoodsList from './foodsListReducer';
import FoodCompare from './foodCompareReducer';
import FoodInfo from './foodInfoReducer';
import Search from './searchReducer';
import User from './userReducer';
export default rootReducer = combineReducers({
Strolling,
Foods,
FoodsList,
FoodCompare,
FoodInfo,
Search,
User,
})
// export default rootReducer = combineReducers({
// Strolling:Strolling,
// Foods:Foods,
// FoodsList:FoodsList,
// FoodCompare:FoodCompare,
// FoodInfo:FoodInfo,
// Search:Search,
// User:User,
// })
// export default function rootReducer(state = {},action){
// return{
// Strolling: Strolling(state.Strolling,action),
// Foods:Foods(state.Foods,action),
// FoodsList:FoodsList(state.FoodsList,action),
// FoodCompare:FoodCompare(state.FoodCompare,action),
// FoodInfo:FoodInfo(state.FoodInfo,action),
// Search:Search(state.Search,action),
// User:User(state.User,action)
// }
// }
//以上三種方式是等價的,key可以設置也可以省略
注意:我們不一定非要用combineReducers來組合子reducer,我們可以自定義類似功能的方法來組合,state的結構完全由我們決定。
store
一個應用只有一個store,store 就是用來維持應用所有的 state 樹 的一個對象。 改變 store 內 state 的惟一途徑是對它 dispatch 一個 action,它有三個函數
-
getState()
返回應用當前的 state 樹。 -
dispatch(action)
分發 action。這是觸發 state 變化的惟一途徑。
會使用當前 getState() 的結果和傳入的 action 以同步方式的調用 store 的 reduce 函數。返回值會被作為下一個 state。從現在開始,這就成為了 getState() 的返回值,同時變化監聽器(change listener)會被觸發。 -
subscribe(listener)
當state樹發生變化的時候store會調用subscribe函數,我們可以傳一個我們訂制的函數作為參數來進行處理
參數:一個函數
返回值:返回一個解綁定函數//添加監聽 let unsubscribe = store.subscribe(handleChange) //解除監聽 unsubscribe()
-
replaceReducer(nextReducer)
替換 store 當前用來計算 state 的 reducer。
這是一個高級 API。只有在你需要實現代碼分隔,而且需要立即加載一些 reducer 的時候才可能會用到它。在實現 Redux 熱加載機制的時候也可能會用到。
react-redux基礎
前言已經提到過react-redux的由來,這里在啰嗦一下,react-redux是redux作者專門為react訂制的,這樣使用起來更方便,我們只需在我們的組件中通過屬性props獲取dispatch方法,就可以直接向store發送一個action,而不需要再獲取store對象,通過store.dispatch方法發送。
react-redux有兩寶,provider和connect,下面詳細介紹一下。
Provider:
有一個store屬性,我們要將應用的根組件放到Provider標簽中,這樣應用的所有的子組件就可以通過context來獲取store對象了,但是我們一般不會通過此法來獲取store對象,Provider是為了給connect函數使用的,這樣才能通過connect函數的參數獲取到store的state和dispatch了。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect是一個高階函數,connect()
本身會返回一個函數變量(假如名字為func),給這個函數變量傳遞一個參數func(MainContainer)會生成一個MainContainer容器組件,形如下面的寫法:
export default connect((state) => {
const { Main } = state;
return {
Main
}
})(MainContainer);
參數一:[mapStateToProps(state, [ownProps]): stateProps] (Function)
如果定義該參數,組件將會監聽 Redux store 的變化。任何時候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props 合并。如果你省略了這個參數,你的組件將不會監聽 Redux store。如果指定了該回調函數中的第二個參數 ownProps,則該參數的值為傳遞到組件的 props,而且只要組件接收到新的 props,mapStateToProps 也會被調用(例如,當 props 接收到來自父組件一個小小的改動,那么你所使用的 ownProps 參數,mapStateToProps 都會被重新計算)。
參數二:[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function):
如果傳遞的是一個對象,那么每個定義在該對象的函數都將被當作 Redux action creator,而且這個對象會與 Redux store 綁定在一起,其中所定義的方法名將作為屬性名,合并到組件的 props 中。如果傳遞的是一個函數,該函數將接收一個 dispatch 函數,然后由你來決定如何返回一個對象,這個對象通過 dispatch 函數與 action creator 以某種方式綁定在一起(提示:你也許會用到 Redux 的輔助函數 bindActionCreators())。
如果你省略這個 mapDispatchToProps 參數,默認情況下,dispatch 會注入到你的組件 props 中。
如果指定了該回調函數中第二個參數 ownProps,該參數的值為傳遞到組件的 props,而且只要組件接收到新 props,mapDispatchToProps 也會被調用。
參數三:[mergeProps(stateProps, dispatchProps, ownProps): props] (Function)
如果指定了這個參數,mapStateToProps() 與 mapDispatchToProps() 的執行結果和組件自身的 props 將傳入到這個回調函數中。該回調函數返回的對象將作為 props 傳遞到被包裝的組件中。你也許可以用這個回調函數,根據組件的 props 來篩選部分的 state 數據,或者把 props 中的某個特定變量與 action creator 綁定在一起。如果你省略這個參數,默認情況下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結果。
[options] (Object) 如果指定這個參數,可以定制 connector 的行為。
參數四:[options] (Object) 如果指定這個參數,可以定制 connector 的行為。
[pure = true] (Boolean): 如果為 true,connector 將執行 shouldComponentUpdate 并且淺對比 mergeProps 的結果,避免不必要的更新,前提是當前組件是一個“純”組件,它不依賴于任何的輸入或 state 而只依賴于 props 和 Redux store 的 state。默認值為 true。
[withRef = false] (Boolean): 如果為 true,connector 會保存一個對被包裝組件實例的引用,該引用通過 getWrappedInstance() 方法獲得。默認值為 false。
redux-redux使用
上面說了provider和connect方法,下面是實用講解
創建store對象的js文件
下面的代碼里包括應用中間件redux-thunk,和創建store對象兩步,這里有更多關于中間件的詳情
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootRudcer';
//使用thunk中間件
let createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
//創建store對象,一個APP只有一個store對象
let store = createStoreWithMiddleware(rootReducer);
export default store;
程序的入口文件
import React from 'react';
import { Provider } from 'react-redux';
import store from './store/store';
import App from './containers/app';
export default class Root extends React.Component {
render() {
return (
//將APP的根視圖組件包含在provider標簽中
<Provider store = {store} >
<App />
</Provider>
)
}
}
在容器組件中,將redux和組件關聯起來生成一個容器組件,這里是redux與組件關聯的地方,大多數童鞋使用redux最迷惑的地方估計就在這一塊了。
import React from 'react';
import {connect} from 'react-redux';
import Brand from '../Components/Brand';
//BrandContainer容器組件
class BrandContainer extends React.Component {
render() {
return (
//把容器組件的屬性傳遞給UI組件
<Brand {...this.props} />
)
}
}
export default connect((state) => {
const { BrandReducer } = state;
return {
BrandReducer
}
})(BrandContainer);
這樣UI組件Brand中就可以通過屬性獲取dispatch方法以及處理后的最新state了
const {dispatch, BrandReducer} = this.props;
下面來解釋一下上面的代碼
將當前的BrandContainer組件關聯起來,上面介紹了store中的state對象的結構會是這樣的:
{
reducer1: ...
reducer2: ...
}
所以可以通過解構的方式,獲取對應模塊的state,如下面的const { BrandReducer } = state;
下面這一塊代碼的作用就是將store中state傳遞給關聯的容器組件中,當store中的state發生變化的時候,connect的第一參數mapStateToProps回調函數就會被調用,并且將該回調函數的返回值映射成其關聯組件的一個屬性,這樣容器組件的屬性就會發生變化,而UI組件又通過{...this.props}將容器組件的屬性傳遞給了UI組件,所以UI組件的屬性也會發生變化,我們知道屬性的變化會導致UI組件重新render。好了,我們就能知道為什么我們在UI組件中dispatch一個action后UI組件能更新了,因為UI組件的屬性發生變化導致RN重繪了UI。
react native 組件的生命周期
弄明白了這個圖我認為你就能基本掌握RN了(圖片來自互聯網,【注意圖中有錯誤】最后end的時候是componentWillUnmount
)
項目的推薦目錄
這種結構適合業務邏輯不太復雜的中小型項目,其優點是邏輯模塊清晰,缺點是文件目錄跨度較大,對于大型項目建議按項目的功能模塊來劃分。
【小型項目建議結構】
utils -- 定義一些工具類,比如網絡請求、本地緩存類、加解密工具類等
common -------- 定義一些通用樣式,以及通用常量,如actionType等
components ----- 定義一些通用組件
pages ------------- 業務組件
containers ------- 定義redux的容器組件
actions ------------ 定義一系列action creator
reduces ---------- 定義reducer
store --------------- 定義把子reducer綁定成一個rootReducer來創建store
navigator---------- 注冊路由
root.js ------------ 根組件
【復雜項目建議結構】
utils -- 定義一些工具類,比如網絡請求、本地緩存類、加解密工具類等
common -------- 定義一些通用樣式,以及通用常量,如actionType等
components ----- 定義一些通用組件
業務模塊一:
pages ------------- 業務組件
containers ------- 定義redux的容器組件
actions ------------ 定義一系列action creator
reduces ---------- 定義reducer
業務模塊二:
pages ------------- 業務組件
containers ------- 定義redux的容器組件
actions ------------ 定義一系列action creator
reduces ---------- 定義reducer
navigator---------- 注冊路由
store --------------- 定義把子reducer綁定成一個rootReducer來創建store
root.js ------------ 根組件
熱更新
目前市場主流的熱更新庫為code-push和jspatch,但現在發現用了jspatch庫上架AppStore會被拒,所以這里主要用code-push來做講解。
code-push是微軟推出的一個熱更新庫。。。。。。
相關文章
React 實踐心得:react-redux 之 connect 方法詳解
Redux 入門教程(一):基本用法
redux中文文檔
react + redux 完整的項目,同時寫一下個人感悟
注:部分圖片來源于互聯網