前端開發(fā)React用的很多妄均,但它只是一個view,涉及到復(fù)雜的功能時就必須要用到Redux/Mobx等狀態(tài)管理器境肾,React 只是 DOM 的一個抽象層蚂夕,并不是 Web 應(yīng)用的完整解決方案。
參考http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
上一章佩谷,我介紹了 Redux 的基本做法:用戶發(fā)出 Action旁壮,Reducer 函數(shù)算出新的 State,View 重新渲染谐檀。
但是抡谐,一個關(guān)鍵問題沒有解決:異步操作怎么辦?Action 發(fā)出以后稚补,Reducer 立即算出 State臼闻,這叫做同步;Action 發(fā)出以后坎弯,過一段時間再執(zhí)行 Reducer,這就是異步五垮。
怎么才能 Reducer 在異步操作結(jié)束后自動執(zhí)行呢?這就要用到新的工具:中間件(middleware)杜秸。
中間件的概念
為了理解中間件放仗,讓我們站在框架作者的角度思考問題:如果要添加功能,你會在哪個環(huán)節(jié)添加撬碟?
- Reducer:純函數(shù)诞挨,只承擔(dān)計算 State 的功能,不合適承擔(dān)其他功能呢蛤,也承擔(dān)不了惶傻,因為理論上,純函數(shù)不能進行讀寫操作其障。
- View:與 State 一一對應(yīng)银室,可以看作 State 的視覺層,也不合適承擔(dān)其他功能励翼。
- Action:存放數(shù)據(jù)的對象蜈敢,即消息的載體,只能被別人操作汽抚,自己不能進行任何操作抓狭。
想來想去,只有發(fā)送 Action 的這個步驟造烁,即store.dispatch()方法否过,可以添加功能。舉例來說膨蛮,要添加日志功能叠纹,把 Action 和 State 打印出來,可以對store.dispatch進行如下改造敞葛。
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 這兩步之間鸿秆,添加了其他功能。
中間件的用法
本教程不涉及如何編寫中間件怎囚,因為常用的中間件都有現(xiàn)成的卿叽,只要引用別人寫好的模塊即可桥胞。比如,上一節(jié)的日志中間件考婴,就有現(xiàn)成的redux-logger模塊贩虾。這里只介紹怎么使用中間件。
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
上面代碼中沥阱,redux-logger提供一個生成器createLogger缎罢,可以生成日志中間件logger。然后考杉,將它放在applyMiddleware方法之中策精,傳入createStore方法,就完成了store.dispatch()的功能增強崇棠。
這里有兩點需要注意:
- createStore方法可以接受整個應(yīng)用的初始狀態(tài)作為參數(shù)咽袜,那樣的話,applyMiddleware就是第三個參數(shù)了枕稀。
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
);
- 中間件的次序有講究酬蹋。
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
上面代碼中,applyMiddleware方法的三個參數(shù)抽莱,就是三個中間件。有的中間件有次序要求骄恶,使用前要查一下文檔食铐。比如,logger就一定要放在最后僧鲁,否則輸出結(jié)果會不正確虐呻。
applyMiddlewares()
看到這里,你可能會問寞秃,applyMiddlewares這個方法到底是干什么的斟叼?
它是 Redux 的原生方法,作用是將所有中間件組成一個數(shù)組春寿,依次執(zhí)行朗涩。下面是它的源碼。
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {...store, dispatch}
}
}
上面代碼中绑改,所有中間件被放進了一個數(shù)組chain谢床,然后嵌套執(zhí)行,最后執(zhí)行store.dispatch厘线∈锻龋可以看到,中間件內(nèi)部(middlewareAPI)可以拿到getState和dispatch這兩個方法造壮。
異步操作的基本思路
理解了中間件以后渡讼,就可以處理異步操作了。
同步操作只要發(fā)出一種 Action 即可,異步操作的差別是它要發(fā)出三種 Action成箫。
- 操作發(fā)起時的 Action
- 操作成功時的 Action
- 操作失敗時的 Action
以向服務(wù)器取出數(shù)據(jù)為例展箱,三種 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 重新渲染
- 操作結(jié)束后,再送出一個 Action檬洞,觸發(fā) State 更新為"操作結(jié)束"狀態(tài)狸膏,View 再一次重新渲染
redux-thunk 中間件
異步操作至少要送出兩個 Action:用戶觸發(fā)第一個 Action,這個跟同步操作一樣添怔,沒有問題湾戳;如何才能在操作結(jié)束時,系統(tǒng)自動送出第二個 Action 呢广料?
奧妙就在 Action Creator 之中砾脑。
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(fetchPosts(selectedPost))
}
// ...
上面代碼是一個異步組件的例子。加載成功后(componentDidMount方法)艾杏,它送出了(dispatch方法)一個 Action韧衣,向服務(wù)器要求數(shù)據(jù) fetchPosts(selectedSubreddit)。這里的fetchPosts就是 Action Creator购桑。
下面就是fetchPosts的代碼汹族,關(guān)鍵之處就在里面。
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))榴徐,然后進行異步操作守问。拿到結(jié)果后,先將結(jié)果轉(zhuǎn)成 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))柒莉,表示操作開始闻坚。
- 異步操作結(jié)束之后,再發(fā)出一個 Action(receivePosts(postTitle, json))兢孝,表示操作結(jié)束窿凤。
這樣的處理,就解決了自動發(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)
);
上面代碼使用redux-thunk中間件腹备,改造store.dispatch,使得后者可以接受函數(shù)作為參數(shù)斤蔓。
中間件里通過一個action類型的判斷植酥,輕松的處理了store.dispatch參數(shù)只能是對象,不能為函數(shù)的問題弦牡。
thunk中間件源碼如下:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
因此友驮,異步操作的第一種解決方案就是,寫出一個返回函數(shù)的 Action Creator驾锰,然后使用redux-thunk中間件改造store.dispatch卸留。
redux-promise 中間件
既然 Action Creator 可以返回函數(shù),當然也可以返回其他值椭豫。另一種異步操作的解決方案耻瑟,就是讓 Action Creator 返回一個 Promise 對象旨指。
這就需要使用redux-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()
});
});
寫法二,Action 對象的payload
屬性是一個 Promise 對象魏保。這需要從redux-actions
模塊引入createAction
方法熬尺,并且寫法也要變成下面這樣。
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())
));
}
上面代碼中囱淋,第二個dispatch
方法發(fā)出的是異步 Action猪杭,只有等到操作結(jié)束,這個 Action 才會實際發(fā)出妥衣。注意皂吮,createAction
的第二個參數(shù)必須是一個 Promise 對象。
看一下redux-promise
的源碼税手,就會明白它內(nèi)部是怎么操作的蜂筹。
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}
從上面代碼可以看出,如果 Action 本身是一個 Promise芦倒,它 resolve 以后的值應(yīng)該是一個 Action 對象艺挪,會被dispatch方法送出(action.then(dispatch)),但 reject 以后不會有任何動作兵扬;如果 Action 對象的payload屬性是一個 Promise 對象麻裳,那么無論 resolve 和 reject,dispatch方法都會發(fā)出 Action器钟。