Redux介紹
單一數據源
整個單頁應用的 state 都被儲存在store的內部,可以通過store.getState()獲取,再作為props傳給對應的組件。
state應該盡量少嵌套扁平化,通過id相互引用數據。
import { createStore } from 'redux'; const store = createStore(reducer); //stroe需要reducer計算新的state //createStore函數的簡單實現 const createStore = (reducer) => { let state; let listeners = []; //返回store中的state tree的API const getState = () => state; //store.dispatch()會自動調用reducer生成新的state,並執行注冊的回調函數 const dispatch = (action) => { //調用reducer更新state state = reducer(state, action); listeners.forEach(listener => listener()); }; //注冊回調函數,通常是render或setState const subscribe = (listener) => { listeners.push(listener); return () => { //注銷回調函數 listeners = listeners.filter(l => l !== listener); } }; dispatch({}); return { getState, dispatch, subscribe }; };
State 為只讀
為防止出現race condition和deep equal的遍歷,state應為只讀,不能直接修改state,每個state都是新對象,而改變 state 的方法只有dispatch action。
使用純函數reducer來修改state
reducer 是純函數,接收prestate 和 action,返回新的 state。reducer必須保證同樣的參數輸入其輸出一樣,所以redux規定在Reducer函數里面不能直接改變 State,必須返回新的對象。在reducer中,不能改寫參數,不能調用系統 I/O 的API,不能調用Date.now()或者Math.random()等不純的方法,只是單純的執行計算。例如 API 調用或路由跳轉等有副作用的操作應該在 dispatch action 前發生。
function reducer(state, action) { switch (type) { case ADD_CHAT: //若state 是一個對象,不能直接修改state,而應使用如下方式返回新建的state return Object.assign({}, state, { newState }); // 或者return { ...state, ...newState }; default: return state; //如果action不屬於該reducer負責,需要返回原state } } // 若State 是一個數組 function reducer(state, action) { return [...state, newItem]; //或者 return […state].concat(newItem); }
也可以使用immutable.js把 State 對象設成只讀,這樣改變State時只能通過生成一個新對象。
因為reducer函數的作用只是接收一個state計算后返回一個新的state,所以在應用中可以將多個分布在不同目錄的reducer(函數)合成一個rootReducer,每個小的reducer只負責管理全局store中對應的一部分state:
export default function todoAppReducer(state = {}, action) { return { //只負責store中visibilityFilter和todos字段的state,別的state不可訪問 visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todosReducer(state.todos, action) //state.todos是大state中todosReducer負責的state } }
//等價於
import { combineReducers } from 'redux'; const todoAppReducer = combineReducers({ visibilityFilter, //store的state樹中的節點名和reducer函數名相同 todos:todosReducer // 設置不同的對應名字 }) export default todoApp;
//combineReducer將子reducer合成一個大reducer。每個 reducer 根據 key 來篩選出 state 中的一部分數據並處理。最后這個生成的函數再將所有 reducer 的結果合並成一個大的對象,從而更新store。 const combineReducers = reducers => { return (state = {}, action) => { return Object.keys(reducers).reduce( (nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {} ); }; };
array.reduce(function(accumulator, currentValue, currentIndex, array), initialValue)
注意:若無initialValue,reduce 會跳過索引0從索引1開始執行 callback,若提供 initialValue,則從索引0開始。
如果需要在一個 Reducer 中訪問另外一個 Reducer 負責的 state,這時需要自己寫root Reducer或用reduce-reducers來控制。
state可以嵌套,對應的reducer亦可以嵌套,最終由combineReducer方法生成finalReducers,對應的state結構和reducer的嵌套結構相同。
action 是一個用於抽象描述已發生事件的普通對象,可以被序列化,是 store 數據的唯一來源。應盡量減少在 action 中傳遞的數據。
action Creator
View要發送多少種消息,就會有多少種 Action。可以定義一個Action Creato函數來生成 Action,不用每次都寫樣板代碼。
const ADD_TODO = '添加 TODO'; function addTodo(text) { return { type: ADD_TODO, text } } store.dispatch( addTodo('Learn Redux') ); //store會自動調用reducer得到新的state store.dispatch( addTodo('Learn React) );
可以直接通過 store.dispatch() 調用 dispatch() 方法,也可以用 react-redux 包提供的 connect()方法調用,更方便。
@connect() es7的類修飾器
React-Redux
Redux 和 React 之間沒有直接關系,可以在react中直接使用store的方法,也可以通過react-redux模塊進行綁定。
容器組件 |
展示組件 |
|
Location |
最頂層,路由處理 |
中間和子組件,可復用 |
Aware of Redux |
是 |
否 |
讀取數據 |
從 Redux 獲取 state |
從 props 獲取數據 |
修改數據 |
向 Redux 派發 actions |
從 props 調用回調函數 |
容器組件Container Components:使用redux的API進行數據管理,帶有內部狀態。
展示組件Presentational Components:不使用任何 Redux 的 API,通過props傳遞數據,負責UI呈現,沒有狀態(不使用this.state這個變量)。
connect方法
從 UI 組件生成容器組件(UI component => container container),將react (ui)和redux (store)綁定起來,可以在嵌套的不同層次使用。
/*container component *容器組件負責管理數據和業務邏輯,UI組件只顯示視圖 */ import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' //mapStateToProps將state映射到UI組件的參數(props),返回一個對象,其屬性代表 UI 組件的同名參數。 //mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,最后觸發 UI 組件的重新渲染。如果不傳入mapStateToProps參數,store的更新不會引起UI組件的更新。 const mapStateToProps = (state, ownProps) =>{ //state指reduce負責的那部分state,ownProps指容器父組件傳入的所有屬性 //active是傳給子組件的prop屬性,父組件傳入的其他props也會在子組件中 return{ active: ownProps.filter === state.visibilityFilter } } //如果mapDispatchToProps為函數,則應返回一個對象,其每個鍵值對都是一個映射,定義了用戶對 UI 組件的操作怎樣分發出 Action。 //若mapDispatchToProps是一個對象,則屬性名對應UI組件的同名參數,屬性值是一個當作Action creator的函數,其返回的action會由Redux自動發出給store。 const mapDispatchToProps = (dispatch, ownProps) => ({ //onClick是傳給子組件調用的方法,在view觸發action時調用 onClick: () => { dispatch(setVisibilityFilter(ownProps.filter)) } }) //如果不傳入mapStateToProps/mapStateToProps,將默認只傳入dispatch和父組件傳入的props,不傳入store const FilterLink = connect( // FilterLink是容器組件 mapStateToProps, // 將全局state對象映射到 UI 組件 mapDispatchToProps // UI 組件將傳出action對象的方法 )(Link) //Link為UI組件 export default FilterLink
/*UI 組件,只負責UI的呈現*/ import React from 'react' //active和onClick是父容器組件通過connect方法傳入的props屬性 const Link = ({ active, children, onClick }) => { if (active) { return <span>{children}</span> } return ( <a href="#" onClick={e => { e.preventDefault() onClick() }} > {children} </a> ) } export default Link
<Provider> 組件
使用Provider組件包裹根組件,將store傳入容器組件,並通過組件的context屬性使嵌套的所有子組件都能獲得根state而不用一級一級傳遞下去。
//在任意組件引用全局store,盡量不要直接操作store的實例 Link.contextTypes = { store: React.PropTypes.object }