依照慣例,開頭先放出redux中文文檔地址
異步場景的基本思路
之前討論的所有場景都是同步場景,實際開發中肯定有很多異步場景,下面討論一下異步場景的問題。
首先,異步場景和同步場景本質上還是利用action
、reducer
、store
這些東西,只是設計reducer
和action
的時候更加巧妙,考慮到了異步狀態。
對應不同的action
,在reducer
中添加專門表現異步狀態的內容。
function post(
state = {
isFetching: false,
items: []
},
action
) {
switch(action.type) {
case REQUEST:
return Object.assign({}, state, {
isFetching: true,
})
case RECEIVE:
return Object.assign({}, state, {
isFetching: false,
items: action.posts
})
default:
return state
}
}
搞定了reducer
還要考慮異步場景的action
,我們先不仔細探究代碼實現,只要想明白如果store.dispatch(someAction)
,然后按照異步回調的順序觸發上面的reducer
,那么異步場景就實現了。
解決異步場景
上面我們籠統地講了異步場景的實現思路,目前看來reducer
部分已經沒有大問題和同步場景十分類似,action
部分和同步場景有所區別。
簡單思考可以發現,我們需要在action creator
函數中實現異步,即當我們調用一個action creator
時,不是簡單需要返回一個action
對象,而是根據異步的場景返回不同的action
。看起來我們需要改造action
。
理解了這個問題,就找到了異步場景的癥結。
redux
有許多配套的中間件(middleware
),關于中間件的概念可以查看上面的文檔,我們不仔細討論。簡要地說,中間件在調用action creator
后起作用,可以有多個中間件存在,數據流會在中間件之間傳遞,數據流經過所有中間件處理流出時應該還是一個簡單的action
對象,到達store.dispatch(action)
時所有內容應該和同步數據流一致。
redux-thunk 實現
redux-thunk
改造了store.dispatch
,使action creator
能接收一個函數作為參數。改造完action creator
之后,就可以創造一個內部根據狀態再次異步操作的action
。
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
結合一個小例子來看一下
已有一個container
組件
class example extends Component {
constructor() {
super();
}
componentDidMount() {
組件需要調用后端API載入數據
this.props.getName();
}
render () {
console.log(this.props)
return (
<div>
// 組件按照不同狀態顯示‘querying...’/正常數據
{ this.props.info.server.isFetching ?
(
<span>
querying...
</span>
)
:
(
// 點擊請求API刷新狀態
<span onClick={() => this.props.getName()}>
{ this.props.info.server.name }
</span>
)
}
...
</div>
);
}
}
const mapStateToProps = (state) => {
return {
info: state.info
}
};
const mapDispatchToProps = (dispatch) => {
return {
getName: () => {
dispatch(getServerInfo());
}
};
};
const Example = connect(
mapStateToProps,
mapDispatchToProps
)(example);
export default Example;
下面開始實現這個小功能
編寫對應的action
function fetchServerInfo() {
return {
type: 'GET SERVER INFO'
};
}
function receiveServerInfo({ payload: server }) {
return {
type: 'RECEIVE SERVER INFO',
payload: server
};
}
// 這是一個改造后的action creator,可以異步dispatch(action)
function getServerInfo() {
return (dispatch, getState) => {
dispatch(fetchServerInfo());
return fetch(`http://server/info`)
.then(
response => response.json(),
error => console.log('an error', error)
)
.then(
(json) => {
let { server } = json;
dispatch(receiveServerInfo({ payload: server }));
}
)
}
}
編寫reducer
// 加入一個isFetching的狀態標志位
const initialInfo = {
server: {
isFetching: false,
name: 'localhost',
status: ''
}
};
function setServerName (state = initialInfo, action) {
let server = state.server;
switch (action.type) {
case 'GET SERVER INFO':
return Object.assign(
{},
server,
{
isFetching: true
}
);
case 'RECEIVE SERVER INFO':
let { payload: { name } } = action;
return Object.assign(
{},
server,
{
isFetching: false,
name
}
);
default:
return server
}
}
function info (state = initialInfo, action) {
return {
server: setServerName(state.title, action)
}
}
export default info;
添加middleware
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleWare from 'redux-thunk';
...
const store = createStore(
reducer,
applyMiddleware(
thunkMiddleWare
)
);
redux-saga 實現
redux-saga
是一種不一樣的實現方式,它沒有在action creator
上做文章,它監聽了action
,如果action
中有effect
,那么它可以調用Genertor
函數實現異步。
還是結合上面的小例子來看一下:
已有一個container
組件
class example extends Component {
constructor() {
super();
}
componentDidMount() {
組件需要調用后端API載入數據
this.props.getName();
}
render () {
console.log(this.props)
return (
<div>
// 組件按照不同狀態顯示‘querying...’/正常數據
{ this.props.info.server.isFetching ?
(
<span>
querying...
</span>
)
:
(
// 點擊請求API刷新狀態
<span onClick={() => this.props.getName()}>
{ this.props.info.server.name }
</span>
)
}
...
</div>
);
}
}
const mapStateToProps = (state) => {
return {
info: state.info
}
};
const mapDispatchToProps = (dispatch) => {
return {
getName: () => {
dispatch(getServerInfo());
}
};
};
const Example = connect(
mapStateToProps,
mapDispatchToProps
)(example);
export default Example;
創建一個saga
文件監聽action
import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import requestServerInfo from '../../services/info'
// 遇到被監聽的action會調用下面的函數,并將異步操作分解
function* getServerInfo() {
try {
yield put({ type: 'GET SERVER INFO' });
const { data: { server: server } } = yield call(requestServerInfo, 'http://server/test');
yield put({ type: 'RECEIVE SERVER INFO', payload: { server } });
} catch (e) {
yield put({ type: 'RECEIVE AN ERROR' });
}
}
// 在此處使用takeEvery監聽action
function* infoSaga() {
yield* takeEvery('QUERY_INFO', getServerInfo);
}
export { infoSaga };
在store
中應用saga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { infoSaga } from '../sagas/info';
import reducer from '../reducers/all';
// 引入redux-saga中間件
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(
sagaMiddleware
)
);
sagaMiddleware.run(infoSaga);
export default store;
剩下reducer
部分和上面一樣處理加入isFetching
標志位即可,沒有變化