設計思想
核心概念
- 所有的狀態存放在
Store
。元件每次重新渲染,都必須由狀態變化引起。 - 使用者在 UI 上發出
action
。 reducer
函式接收action
,然後根據當前的state
,計算出新的state
。
動機
- 隨著 JavaScript 單頁應用開發日趨複雜,JavaScript 需要管理比任何時候都要多的
state
(狀態)。通過限制更新發生的時間和方式,Redux 試圖讓state
的變化變得可預測。
生命週期
Redux 應用中資料的生命週期遵循下面 4 個步驟:
呼叫 store.dispatch(action)。
- Action 就是一個描述“發生了什麼”的普通物件。比如:
{ type: 'LIKE_ARTICLE', articleId: 42 };
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } };
{ type: 'ADD_TODO', text: 'Read the Redux docs.'};複製程式碼
- 可以把
action
理解成新聞的摘要。如 “瑪麗喜歡42號文章。” 或者 “任務列表裡新增了'學習 Redux 文件'”。
你可以在任何地方呼叫store.dispatch(action)
,包括元件中、XHR 回撥中、甚至定時器中。
Redux store 呼叫傳入的 reducer 函式。
- Store 會把兩個引數傳入
reducer
: 當前的state
樹和action
。例如,在這個todo
應用中,根reducer
可能接收這樣的資料:
// 當前應用的 state(todos 列表和選中的過濾器)
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [
{
text: 'Read the docs.',
complete: false
}
]
}
// 將要執行的 action(新增一個 todo)
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
}
// render 返回處理後的應用狀態
let nextState = todoApp(previousState, action);複製程式碼
- 注意 reducer 是純函式。它僅僅用於計算下一個 state。它應該是完全可預測的:多次傳入相同的輸入必須產生相同的輸出。它不應做有副作用的操作,如 API 呼叫或路由跳轉。這些應該在
dispatch(action)
前發生。
根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
- 根
reducer
的結構完全由你決定。Redux 原生提供combineReducers()
輔助函式,來把根reducer
拆分成多個函式,用於分別處理state
樹的一個分支。
-下面演示 combineReducers() 如何使用。假如你有兩個 reducer:一個是 todo 列表,另一個是當前選擇的過濾器設定:
function todos(state = [], action) {
// 省略處理邏輯...
return nextState;
}
function visibleTodoFilter(state = 'SHOW_ALL', action) {
// 省略處理邏輯...
return nextState;
}
let todoApp = combineReducers({
todos,
visibleTodoFilter
})複製程式碼
- 當你觸發
action
後,combineReducers
返回的todoApp
會負責呼叫兩個 reducer:
let nextTodos = todos(state.todos, action);
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);複製程式碼
- 然後會把兩個結果集合併成一個 state 樹:
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
};複製程式碼
- 雖然
combineReducers()
是一個很方便的輔助工具,你也可以選擇不用;你可以自行實現自己的根reducer
!
Redux store 儲存了根 reducer 返回的完整 state 樹。
這個新的樹就是應用的下一個 state!所有訂閱 store.subscribe(listener) 的監聽器都將被呼叫;監聽器裡可以呼叫 store.getState() 獲得當前 state。
現在,可以應用新的
state
來更新 UI。如果你使用了 React Redux 這類的繫結庫,這時就應該呼叫component.setState(newState)
來更新。
三大原則
單一資料來源
- 整個應用的
state
被儲存在一棵object tree
中,並且這個object tree
只存在於唯一一個store
中。
State 是隻讀的
- 惟一改變
state
的方法就是觸發action
,action
是一個用於描述已發生事件的普通物件。
使用純函式來執行修改
- 為了描述
action
如何改變state tree
,你需要編寫reducers
。
Redux 核心API
- Redux的核心是一個
store
,這個store
有Redux提供的createStore(reducers,[initialState])
方法生成。從函式簽名看出,想生成store
,必須傳入reducers
,同時也可以傳入第二個可選引數初始化狀態initialState
。
createStore
使用方法
const store = createStore(reducer);
reducer
- 在Redux裡,負責響應
action
並修改資料的角色就是reducer
。reducer
本質上是一個純函式,其函式簽名為reducer(previousState, action) => newState
。reducer
在處理action
時,需傳入一個previousState
引數。reducer
的職責就是根據previousState
和action
來計算出新的newState
。 使用方法將
reducer
即下面的todo
作為引數傳入createStore(todo)中//以下為reducer的格式 const todo = (state = initialState, action) => { switch(action.type) { case 'XXX': return //具體的業務邏輯; case 'XXX': return //具體的業務邏輯; default: return state; } }複製程式碼
- 在Redux裡,負責響應
getState()
- 使用方法
getState()
- 獲取
store
中的狀態。
- 使用方法
dispatch(action)
- 使用方法
store.dispatch(action)
- 分發一個
action
,並返回這個action
,這是唯一能改變store
中資料的方式。store.dispatch接受一個Action物件作為引數,將它傳送出去。
- 使用方法
subscribe(listener)
- 使用方法
store.subscribe(listenter)
- 註冊一個監聽者,它在
store
發生變化時被呼叫,一旦State發生了變化,就會自動執行這個函式。通過subscribe繫結了一個監聽函式之後,只要dispatch了一個action,所有監聽函式都會自動執行一遍。
- 使用方法
replaceReducer(nextReducer)
- 更新當前
store
裡的reducer
,一般只會在開發者模式中呼叫該方法。
- 更新當前
createStore的實現
包含
getState()
,dispatch(action)
,subscribe(listener)
;本函式近似原始碼,可簡單實現功能與幫助理解createStore
的原理const createStore = (reducer) => { let state; //宣告一個變數承接狀態 let list = [];//宣告一個陣列用於儲存監聽函式 const getState = () => { return state;//直接返回state; } const dispatch = (action) =>{ state = reducer(state, action);//更新狀態,且迴圈list陣列,並執行裡面的事件 list.forEach((fn) => { fn(); }) } const subscribe = (fn) => { list.push(fn);//將函式傳入list中 return () => { list = list.filter(cd => cd != fn) } } return { getState, subscribe, dispatch } }複製程式碼
combineReducers
- 隨著應用變得複雜,需要對
reducer
函式 進行拆分,拆分後的每一塊獨立負責管理state
的一部分。combineReducers
輔助函式的作用是,把一個由多個不同reducer
函式作為value
的object
,合併成一個最終的reducer
函式,然後就可以對這個reducer
呼叫createStore
。
- 隨著應用變得複雜,需要對
使用方法
- 將多個不同的
reducer
作為物件的屬性傳入combineReducers({})
函式中,
- 將多個不同的
const rootReducer = combineReducers({
reducer1,
reducer2,
...
})複製程式碼
返回值
(Function):一個呼叫 reducers 物件裡所有 reducer 的 reducer,並且構造一個與 reducers 物件結構相同的 state 物件。
let store = createStore(rootReducer) //store = { reducer1: ... , reducer2: ... , ... } let {reducer1, reducer2} = store; //取出複製程式碼
模擬實現combineReducers
//研究邏輯看這個
const combineReducers = (reducers) => {
return (state = {}, action) => {
let newState = {};
Object.keys(reducers).forEach((key) =>{
newState[key] = reducers[key](state[key], action);
})
return newState;
}
}
//簡寫裝逼看這個
const combineReducers = (reducers) => (state = {}, action) => Object.keys(reducers).reduce((newState, key) => {
newState[key] = reducers[key](state[key], action);
return newState;
},{})複製程式碼
react的跨級元件通訊(蟲洞)幫助理解(react-redux)
- 此方法為React中的方法,隨著應用變的越來越複雜,元件巢狀越來越深,有時要從最外層將一個資料一直傳遞到最裡層。
- 理論上,通過
prop
一層層傳遞下去當然是沒問題的。不過這也太麻煩啦,要是能在最外層和最裡層之間開一個穿越空間的蟲洞就好了。 - React的開發者也意識到這個問題,為我們開發出了這個空間穿越通道 ——
Context
。 注意:
context
一直都在React原始碼中,但在React0.14版本才被記錄官方文件。官方並不太推薦大量使用,雖然它可以減少逐級傳遞,但當元件複雜時,我們並不知道context
是從哪傳來的。它就類似於全域性變數。context使用方法
在外層定義一個
getChildContext
方法,在父層制定childContextTypes
。class Provider extends Component{ getChildContext() { return {store: ...}; } render(){ return( this.props.children ) } } Provider.childContextTypes = { store : React.PropTypes.object };複製程式碼
在內層設定元件的
contextTypes
後,即可在元件裡通過this.context.
來訪問。class child extends Component{ render(){ const store = this.context.store; return( <div>1</div> ) } } child.contextTypes = { store: React.PropTypes.object }複製程式碼
解讀react-redux
- 前面說到Redux的核心只有一個
createStore()
方法,這樣還不足以讓Redux在我們的react應用中發揮作用,還需要react-redux庫 ———— Redux官方提供的React繫結。 - react-redux提供了一個元件和一個API幫助Redux和React進行繫結,一個是React元件
<Provider />
, 一個是connect()
。關於它們,我們需要知道的是,<Provider />
接受一個store
作為props
,它是整個Redux應用的頂層元件,而connect()
提供了在整個React應用的任意元件中獲取store
中資料的功能。
Provider
其實就是建立一個外層包裹住整個Redux應用。
<Provider />
主要原始碼
export default class Provider extends Component {
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
render() {
return Children.only(this.props.children)
}
}複製程式碼
- 用法
ReactDom.render(
<Provider store = {store}>
<App />
</Provider>,
document.getElementById("root")
)複製程式碼
connect
使用方法
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(TodoApp)
上面程式碼看似那麼長,但其實理解起來不太難,前四個引數是選填屬性,根據需求填入即可。
connect(...)
呼叫後會返回一個函式這個函式可傳一個引數,即你需要繫結的元件。- 模擬實現
connect
函式,只針對前兩個關鍵引數。
const connect = (mapStateToProps, mapDispatchToProps) => {
return (WrapperComponent) => {
class Connect extends Component {
componentDidMount() {
const store = this.context.store;
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
})
}
componentWillUnmount() {
this.unsubscribe();
}
render (){
const store = this.context.store;
const stateProps = mapStateToProps(store.getState());
const dispatchProps = mapDispatchToProps(store.dispatch);
const props = Object.assign({}, stateProps, dispatchProps);
// return <WrapperComponent {...props} />;
return React.createElement(WrapperComponent, props);
}
}
Connect.contextTypes = {
store: React.PropTypes.object
};
return Connect;
}
}複製程式碼
mapStateToProps
官方解釋: 如果定義該引數,元件將會監聽 Redux
store
的變化。任何時候,只要 Reduxstore
發生改變,mapStateToProps
函式就會被呼叫。該回撥函式必須返回一個純物件,這個物件會與元件的props
合併。如果你省略了這個引數,你的元件將不會監聽 Reduxstore
。如果指定了該回撥函式中的第二個引數ownProps
,則該引數的值為傳遞到元件的props
,而且只要元件接收到新的props
,mapStateToProps
也會被呼叫。使用方法(其實裡面第一個引數就是最早在
<Provider store = {store}>
傳入的store
,於是可以在子元件上訪問store
裡的屬性)
const mapStateToProps = (state, [ownProps]) => {
return {
todos : state.todos
}
}複製程式碼
mapDispatchToProps
- 官方解釋: 如果傳遞的是一個物件,那麼每個定義在該物件的函式都將被當作 Redux
action creator
,而且這個物件會與 Reduxstore
繫結在一起,其中所定義的方法名將作為屬性名,合併到元件的props
中。如果傳遞的是一個函式,該函式將接收一個dispatch
函式,然後由你來決定如何返回一個物件,這個物件通過dispatch
函式與action creator
以某種方式繫結在一起(提示:你也許會用到 Redux 的輔助函式 bindActionCreators())。如果你省略這個mapDispatchToProps
引數,預設情況下,dispatch
會注入到你的元件props
中。如果指定了該回撥函式中第二個引數ownProps
,該引數的值為傳遞到元件的props
,而且只要元件接收到新props
,mapDispatchToProps
也會被呼叫。 - 使用方法(用於傳遞方法)。其實總而言之,
mapStateToProps
是用來傳遞屬性狀態的,而mapDispatchToProps
是用來傳遞改變的方法的。
const mapDispatchToProps = (dispatch, [ownProps]) => {
return{
... : () => {
dispatch(...)
}
}
}複製程式碼
mergeProps
mergeProps(stateProps, dispatchProps, ownProps)
可以接受stateProps, dispatchProps, ownProps三個引數。stateProps
就是傳給connect
的第一個引數mapStateToProps
最終返回的props
。dispatchProps
就是傳給connect
的第二個引數mapDispatchToProps
最終返回的props
。- 而
ownProps
則為元件自己的props
。
options
如果指定這個引數,可以定製 connector 的行為。
[pure = true] (Boolean): 如果為
true
,connector
將執行shouldComponentUpdate
並且淺對比mergeProps
的結果,避免不必要的更新,前提是當前元件是一個“純”元件,它不依賴於任何的輸入或state
而只依賴於props
和 Reduxstore
的state
。預設值為true
。[withRef = false] (Boolean): 如果為
true
,connector
會儲存一個對被包裝元件例項的引用,該引用通過getWrappedInstance()
方法獲得。預設值為false
。