Redux 是 JavaScript 狀態(tài)容器,提供可預測化的狀態(tài)管理翠储。
Redux 的創(chuàng)作理念:
- Web 應用是一個狀態(tài)機绘雁,視圖與狀態(tài)是一一對應的。
- 所有的狀態(tài)援所,保存在一個對象里面庐舟。
視圖的每一種變化,都應該對應著一組數(shù)據(jù)住拭,這組數(shù)據(jù)就存放在state里挪略。
而我們每次修改數(shù)據(jù)應該是有記錄的历帚,并且是可維護的,redux就給我們提供了這樣的一個機制杠娱。
Redux 提供createStore這個函數(shù)挽牢,用來生成 Store。
import { createStore } from 'redux';
const store = createStore(fn);
什么是Store摊求?從開發(fā)角度來說就是封裝了數(shù)據(jù)的存儲狀態(tài)及暴露出修改查尋數(shù)據(jù)的API禽拔。
action和dispatch
查
我們想要獲得某時刻的數(shù)據(jù)就可以調(diào)用getState()。
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
改
前面提到了室叉,我們希望修改數(shù)據(jù)是可維護可記錄的睹栖,那么我們就不應該直接對數(shù)據(jù)進行操作。而在redux里茧痕,修改數(shù)據(jù)就叫action野来。
Action 是一個對象。其中的type
屬性是必須的踪旷,表示 Action 的名稱曼氛。其他屬性可以自由設置,社區(qū)有一個規(guī)范可以參考令野。
const action = {
type: 'ADD_TODO',
payload: 'eat dinner'
};
如果我們的應用會很龐大舀患,修改的種類也很多,每次都手寫一個Object雖然沒有什么不行彩掐,但是如果要項目升級,統(tǒng)一修改的時候維護都會非常困難灰追,那么我們可以寫一個函數(shù)來創(chuàng)建action堵幽,這就是action creator:
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
說了半天action,最終我們還是要落實到修改數(shù)據(jù)弹澎,那么有了action如何修改朴下,這個時候就需要使用dispatch(store.dispatch()
是 View 發(fā)出 Action 的唯一方法。):
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'eat dinner'
});
Reducer
仔細看的到這里都會有疑惑苦蒿,dispatch了之后store怎么拿到action的type殴胧?并且怎么更具payload修改數(shù)據(jù)呢?
這就是reducer在起作用佩迟。什么是reducer:Reducer 是一個函數(shù)团滥,它接受 Action 和當前 State 作為參數(shù),返回一個新的 State报强。就是我們:
const store = createStore(fn);
里的fn灸姊。
fn通常長這樣:
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
const state = reducer(1, {
type: 'ADD',
payload: 2
});
store.dispatch方法會觸發(fā) Reducer 的自動執(zhí)行。為此秉溉,Store 需要知道 Reducer 函數(shù)力惯,做法就是在生成 Store 的時候碗誉,將 Reducer 傳入createStore方法。
為什么這個函數(shù)叫做 Reducer 呢父晶?因為它可以作為數(shù)組的reduce方法的參數(shù)哮缺。請看下面的例子,一系列 Action 對象按照順序作為一個數(shù)組甲喝。
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3
這里不理解的可以google下Array.reduce尝苇。
我們這里只是處理了ADD,但是實際業(yè)務我們會處理更多action來更改更改數(shù)據(jù)俺猿,那么這個reducer就會變得非常龐大茎匠。例如:
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
});
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
});
default: return state;
}
};
redux提供了Redux 提供了一個combineReducers方法,用于 Reducer 的拆分押袍。你只要定義各個子 Reducer 函數(shù)诵冒,然后用這個方法,將它們合成一個大的 Reducer谊惭。
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
// 等同于
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
純函數(shù)
Reducer 函數(shù)最重要的特征是汽馋,它是一個純函數(shù)。也就是說圈盔,只要是同樣的輸入豹芯,必定得到同樣的輸出。
純函數(shù)是函數(shù)式編程的概念驱敲,必須遵守以下一些約束铁蹈。
- 不得改寫參數(shù)
- 不能調(diào)用系統(tǒng) I/O 的API
- 不能調(diào)用Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結果
由于 Reducer 是純函數(shù)众眨,就可以保證同樣的State握牧,必定得到同樣的 View。
但也正因為這一點娩梨,Reducer 函數(shù)里面不能改變 State沿腰,必須返回一個全新的對象,請參考下面的寫法狈定。
// State 是一個對象
function reducer(state, action) {
return Object.assign({}, state, { thingToChange });
// 或者
return { ...state, ...newState };
}
// State 是一個數(shù)組
function reducer(state, action) {
return [...state, newItem];
}
store.subscribe()
查和改算是說完了颂龙,實際業(yè)務中通常會有這樣的場景,當改變了這個值纽什,我需要把界面變成xxx的樣子措嵌,這個時候就是用store.subscribe()設置監(jiān)聽函數(shù):
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
store.subscribe方法返回一個函數(shù),調(diào)用這個函數(shù)就可以解除監(jiān)聽芦缰。
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
unsubscribe();
React-Redux
那么redux和react有啥關系铅匹? 答案是他倆沒啥關系。
Redux 支持 React饺藤、Angular包斑、Ember流礁、jQuery 甚至純 JavaScript。
我們之前說過redux罗丰,就是提供了數(shù)據(jù)的管理神帅,并且數(shù)據(jù)和視圖一一對應,那么這一理念就可以使用在react上萌抵,用redux來管理復雜的react數(shù)據(jù)找御,所以redux是可有可無,也沒有必要一上來就使用redux绍填,如果要使用霎桅,最好判別下你的應用是否真的需要,以下是一些真正需要的場景:
- 某個組件的狀態(tài)讨永,需要共享
- 某個狀態(tài)需要在任何地方都可以拿到
- 一個組件需要改變?nèi)譅顟B(tài)
- 一個組件需要改變另一個組件的狀態(tài)
例如:
- 用戶的使用方式復雜
- 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
- 多個用戶之間可以協(xié)作
- 與服務器大量交互滔驶,或者使用了WebSocket
- View要從多個來源獲取數(shù)據(jù)
直接使用redux在react里比較麻煩,于是就出現(xiàn)了react-redux卿闹〗腋猓可以讓redux和react迅速結合。
使用數(shù)據(jù)锻霎,無非還是查改著角,那么我們究竟怎么來操作?
import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);
(1)查-輸入邏輯:外部的數(shù)據(jù)(即state對象)如何轉換為 UI 組件的參數(shù)
(2)改-輸出邏輯:用戶發(fā)出的動作如何變?yōu)?Action 對象旋恼,從 UI 組件傳出去吏口。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
查
mapStateToProps
就是將store里的state轉化為react組件的props:
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
我們在react組件里就可以直接通過this.props.todos
來訪問到redux里store的數(shù)據(jù)了。
改
我們說過了冰更,改數(shù)據(jù)只能dispatch(action)
产徊。
mapDispatchToProps()
用來建立 react組件的參數(shù)到store.dispatch方法的映射。也就是說冬殃,它定義了哪些用戶的操作應該當作 Action囚痴,傳給 Store叁怪。它可以是一個函數(shù)审葬,也可以是一個對象。
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
我們調(diào)用this.props.onClick
即可改變store的數(shù)據(jù)了奕谭。
如果mapDispatchToProps是一個對象涣觉,它的每個鍵名也是對應 UI 組件的同名參數(shù),鍵值應該是一個函數(shù)血柳,會被當作 Action creator 官册,返回的 Action 會由 Redux 自動發(fā)出。舉例來說难捌,上面的mapDispatchToProps寫成對象就是下面這樣膝宁。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
<Provider> 組件
connect方法生成容器組件以后鸦难,需要讓容器組件拿到state對象,才能生成 真正的react組件的參數(shù)员淫。
React-Redux 提供Provider組件合蔽,可以讓容器組件拿到state。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面代碼中介返,Provider
在根組件外面包了一層拴事,這樣一來,App
的所有子組件就默認都可以拿到state
了圣蝎。
它的原理是React
組件的context
屬性刃宵,請看源碼。
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}
這樣redux就真正的和react關聯(lián)上了徘公。
異步操作
Action 發(fā)出以后牲证,Reducer 立即算出 State,這叫做同步步淹;Action 發(fā)出以后从隆,過一段時間再執(zhí)行 Reducer,這就是異步缭裆。
中間件的概念
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
上面代碼中键闺,對store.dispatch進行了重定義,在發(fā)送 Action 前后添加了打印功能澈驼。這就是中間件的雛形辛燥。
中間件就是一個函數(shù),對store.dispatch方法進行了改造缝其,在發(fā)出 Action 和執(zhí)行 Reducer 這兩步之間挎塌,添加了其他功能。
這個而外的功能就叫做中間件内边,但是我們有更官方的方式支持榴都。
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
- createStore方法可以接受整個應用的初始狀態(tài)作為參數(shù),那樣的話漠其,applyMiddleware就是第三個參數(shù)了嘴高。
- 中間件的次序有講究。
異步操作的基本思路
同步操作只要發(fā)出一種 Action 即可和屎,異步操作的差別是它要發(fā)出三種 Action拴驮。
操作發(fā)起時的 Action
操作成功時的 Action
操作失敗時的 Action
// 寫法一:名稱相同,參數(shù)不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 寫法二:名稱不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
除了 Action 種類不同柴信,異步操作的 State 也要進行改造套啤,反映不同的操作狀態(tài)。下面是 State 的一個例子随常。
let state = {
// ...
isFetching: true,
didInvalidate: true,
lastUpdated: 'xxxxxxx'
};
上面代碼中潜沦,State 的屬性isFetching表示是否在抓取數(shù)據(jù)输硝。didInvalidate表示數(shù)據(jù)是否過時扣典,lastUpdated表示上一次更新時間澈吨。
現(xiàn)在吴汪,整個異步操作的思路就很清楚了。
- 操作開始時喇闸,送出一個 Action袄琳,觸發(fā) State 更新為"正在操作"狀態(tài),View 重新渲染
- 操作結束后燃乍,再送出一個 Action唆樊,觸發(fā) State 更新為"操作結束"狀態(tài),View 再一次重新渲染
這就實現(xiàn)了異步操作刻蟹。
redux-thunk 中間件
異步操作至少要送出兩個 Action:用戶觸發(fā)第一個 Action逗旁,這個跟同步操作一樣,沒有問題舆瘪;如何才能在操作結束時片效,系統(tǒng)自動送出第二個 Action 呢?
奧妙就在 Action Creator 之中英古。
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
上面代碼中淀衣,fetchPosts是一個Action Creator(動作生成器),返回一個函數(shù)召调。這個函數(shù)執(zhí)行后膨桥,先發(fā)出一個Action(requestPosts(postTitle)),然后進行異步操作唠叛。拿到結果后只嚣,先將結果轉成 JSON 格式,然后再發(fā)出一個 Action( receivePosts(postTitle, json))艺沼。
這里有些問題:
- fetchPosts返回了一個函數(shù)册舞,而普通的 Action Creator 默認返回一個對象。
- 返回的函數(shù)的參數(shù)是dispatch和getState這兩個 Redux 方法障般,普通的 Action Creator 的參數(shù)是 Action 的內(nèi)容调鲸。
- 在返回的函數(shù)之中,先發(fā)出一個 Action(requestPosts(postTitle))剩拢,表示操作開始线得。
- 異步操作結束之后饶唤,再發(fā)出一個 Action(receivePosts(postTitle, json))徐伐,表示操作結束。
這樣的處理募狂,就解決了自動發(fā)送第二個 Action 的問題办素。但是角雷,又帶來了一個新的問題,Action 是由store.dispatch方法發(fā)送的性穿。而store.dispatch方法正常情況下勺三,參數(shù)只能是對象,不能是函數(shù)需曾。
這時吗坚,就要使用中間件redux-thunk
。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);
因此呆万,異步操作的第一種解決方案就是商源,寫出一個返回函數(shù)的 Action Creator,然后使用redux-thunk中間件改造store.dispatch谋减。
redux-promise 中間件
既然 Action Creator 可以返回函數(shù)牡彻,當然也可以返回其他值。另一種異步操作的解決方案出爹,就是讓 Action Creator 返回一個 Promise 對象庄吼。
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';
const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
);
這個中間件使得store.dispatch方法可以接受 Promise 對象作為參數(shù)。這時严就,Action Creator 有兩種寫法总寻。寫法一,返回值是一個 Promise 對象梢为。
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
import { createAction } from 'redux-actions';
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 發(fā)出同步 Action
dispatch(requestPosts(selectedPost));
// 發(fā)出異步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}