深入理解 Redux 數(shù)據(jù)流和異步過程管理
本文轉(zhuǎn)載自微信公眾號「神光的編程秘籍 」,作者神說要有光zxg。轉(zhuǎn)載本文請聯(lián)系神光的編程秘籍公眾號。
前端框架的數(shù)據(jù)流
前端框架實(shí)現(xiàn)了數(shù)據(jù)驅(qū)動視圖變化的功能,我們用 template 或者 jsx 描述好了數(shù)據(jù)和視圖的綁定關(guān)系,然后就只需要關(guān)心數(shù)據(jù)的管理了。
數(shù)據(jù)在組件和組件之間、組件和全局 store 之間傳遞,叫做前端框架的數(shù)據(jù)流。
一般來說,除了某部分狀態(tài)數(shù)據(jù)是只有某個(gè)組件關(guān)心的,我們會把狀態(tài)數(shù)據(jù)放在組件內(nèi)以外,業(yè)務(wù)數(shù)據(jù)、多個(gè)組件關(guān)心的狀態(tài)數(shù)據(jù)都會放在 store 里面。組件從 store 中取數(shù)據(jù),當(dāng)交互的時(shí)候去通知 store 改變對應(yīng)的數(shù)據(jù)。
這個(gè) store 不一定是 redux、mobox 這些第三方庫,其實(shí) react 內(nèi)置的 context 也可以作為 store。但是 context 做為 store 有一個(gè)問題,任何組件都能從 context 中取出數(shù)據(jù)來修改,那么當(dāng)排查問題的時(shí)候就特別困難,因?yàn)椴⒉恢朗悄膫€(gè)組件把數(shù)據(jù)改壞的,也就是數(shù)據(jù)流不清晰。
正是因?yàn)檫@個(gè)原因,我們幾乎見不到用 context 作為 store,基本都是搭配一個(gè) redux。
所以為什么 redux 好呢?第一個(gè)原因就是數(shù)據(jù)流清晰,改變數(shù)據(jù)有統(tǒng)一的入口。
組件里都是通過 dispatch 一個(gè) action 來觸發(fā) store 的修改,而且修改的邏輯都是在 reducer 里面,組件再監(jiān)聽 store 的數(shù)據(jù)變化,從中取出最新的數(shù)據(jù)。
這樣數(shù)據(jù)流動是單向的,清晰的,很容易管理。
這就像為什么我們在公司里想要什么權(quán)限都要走審批流,而不是直接找某人,一樣的道理。集中管理流程比較清晰,而且還可以追溯。
異步過程的管理
很多情況下改變 store 數(shù)據(jù)都是一個(gè)異步的過程,比如等待網(wǎng)絡(luò)請求返回?cái)?shù)據(jù)、定時(shí)改變數(shù)據(jù)、等待某個(gè)事件來改變數(shù)據(jù)等,那這些異步過程的代碼放在哪里呢?
組件?
放在組件里是可以,但是異步過程怎么跨組件復(fù)用?多個(gè)異步過程之間怎么做串行、并行等控制?
所以當(dāng)異步過程比較多,而且異步過程與異步過程之間也不獨(dú)立,有串行、并行、甚至更復(fù)雜的關(guān)系的時(shí)候,直接把異步邏輯放組件內(nèi)不行。
不放組件內(nèi),那放哪呢?
redux 提供的中間件機(jī)制是不是可以用來放這些異步過程呢?
redux 中間件
先看下什么是 redux 中間件:
redux 的流程很簡單,就是 dispatch 一個(gè) action 到 store, reducer 來處理 action。那么如果想在到達(dá) store 之前多做一些處理呢?在哪里加?
改造 dispatch!中間件的原理就是層層包裝 dispatch。
下面是 applyMiddleware 的源碼,可以看到 applyMiddleware 就是對 store.dispatch 做了層層包裝,最后返回修改了 dispatch 之后的 store。
- function applyMiddleware(middlewares) {
- let dispatch = store.dispatch
- middlewares.forEach(middleware =>
- dispatch = middleware(store)(dispatch)
- )
- return { ...store, dispatch}
- }
所以說中間件最終返回的函數(shù)就是處理 action 的 dispatch:
- function middlewareXxx(store) {
- return function (next) {
- return function (action) {
- // xx
- };
- };
- };
- }
中間件會包裝 dispatch,而 dispatch 就是把 action 傳給 store 的,所以中間件自然可以拿到 action、拿到 store,還有被包裝的 dispatch,也就是 next。
比如 redux-thunk 中間件的實(shí)現(xiàn):
- function createThunkMiddleware(extraArgument) {
- return ({ dispatch, getState }) => next => action => {
- if (typeof action === 'function') {
- return action(dispatch, getState, extraArgument);
- }
- return next(action);
- };
- }
- const thunk = createThunkMiddleware();
它判斷了如果 action 是一個(gè)函數(shù),就執(zhí)行該函數(shù),并且把 store.dispath 和 store.getState 傳進(jìn)去,否則傳給內(nèi)層的 dispatch。
通過 redux-thunk 中間件,我們可以把異步過程通過函數(shù)的形式放在 dispatch 的參數(shù)里:
- const login = (userName) => (dispatch) => {
- dispatch({ type: 'loginStart' })
- request.post('/api/login', { data: userName }, () => {
- dispatch({ type: 'loginSuccess', payload: userName })
- })
- }
- store.dispatch(login('guang'))
但是這樣解決了組件里的異步過程不好復(fù)用、多個(gè)異步過程之間不好做并行、串行等控制的問題了么?
沒有,這段邏輯依然是在組件里寫,只不過移到了 dispatch 里,也沒有提供多個(gè)異步過程的管理機(jī)制。
解決這個(gè)問題,需要用 redux-saga 或 redux-observable 中間件。
redux-saga
redux-saga 并沒有改變 action,它會把 action 透傳給 store,只是多加了一條異步過程的處理。
redux-saga 中間件是這樣啟用的:
- import { createStore, applyMiddleware } from 'redux'
- import createSagaMiddleware from 'redux-saga'
- import rootReducer from './reducer'
- import rootSaga from './sagas'
- const sagaMiddleware = createSagaMiddleware()
- const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
- sagaMiddleware.run(rootSaga)
要調(diào)用 run 把 saga 的 watcher saga 跑起來:
watcher saga 里面監(jiān)聽了一些 action,然后調(diào)用 worker saga 來處理:
- import { all, takeLatest } from 'redux-saga/effects'
- function* rootSaga() {
- yield all([
- takeLatest('login', login),
- takeLatest('logout', logout)
- ])
- }
- export default rootSaga
redux-saga 會先把 action 透傳給 store,然后判斷下該 action 是否是被 taker 監(jiān)聽的:
- function sagaMiddleware({ getState, dispatch }) {
- return function (next) {
- return function (action) {
- const result = next(action);// 把 action 透傳給 store
- channel.put(action); //觸發(fā) saga 的 action 監(jiān)聽流程
- return result;
- }
- }
- }
當(dāng)發(fā)現(xiàn)該 action 是被監(jiān)聽的,那么就執(zhí)行相應(yīng)的 taker,調(diào)用 worker saga 來處理:
- function* login(action) {
- try {
- const loginInfo = yield call(loginService, action.account)
- yield put({ type: 'loginSuccess', loginInfo })
- } catch (error) {
- yield put({ type: 'loginError', error })
- }
- }
- function* logout() {
- yield put({ type: 'logoutSuccess'})
- }
比如 login 和 logout 會有不同的 worker saga。
login 會請求 login 接口,然后觸發(fā) loginSuccess 或者 loginError 的 action。
logout 會觸發(fā) logoutSuccess 的 action。
redux saga 的異步過程管理就是這樣的:先把 action 透傳給 store,然后判斷 action 是否是被 taker 監(jiān)聽的,如果是,則調(diào)用對應(yīng)的 worker saga 進(jìn)行處理。
redux saga 在 redux 的 action 流程之外,加了一條監(jiān)聽 action 的異步處理的流程。
其實(shí)整個(gè)流程還是比較容易理解的。理解成本高一點(diǎn)的就是 generator 的寫法了:
比如下面這段代碼:
- function* xxxSaga() {
- while(true) {
- yield take('xxx_action');
- //...
- }
- }
它就是對每一個(gè)監(jiān)聽到的 xxx_action 做同樣的處理的意思,相當(dāng)于 takeEvery:
- function* xxxSaga() {
- yield takeEvery('xxx_action');
- //...
- }
但是因?yàn)橛幸粋€(gè) while(true),很多同學(xué)就不理解了,這不是死循環(huán)了么?
不是的。generator 執(zhí)行后返回的是一個(gè) iterator,需要另外一個(gè)程序調(diào)用 next 方法才會繼續(xù)執(zhí)行。所以怎么執(zhí)行、是否繼續(xù)執(zhí)行都是由另一個(gè)程序控制的。
在 redux-saga 里面,控制 worker saga 執(zhí)行的程序叫做 task。worker saga 只是告訴了 task 應(yīng)該做什么處理,通過 call、fork、put 這些命令(這些命令叫做 effect)。
然后 task 會調(diào)用不同的實(shí)現(xiàn)函數(shù)來執(zhí)行該 worker saga。
為什么要這樣設(shè)計(jì)呢?直接執(zhí)行不就行了,為啥要拆成 worker saga 和 task 兩部分,這樣理解成本不就高了么?
確實(shí),設(shè)計(jì)成 generator 的形式會增加理解成本,但是換來的是可測試性。因?yàn)楦鞣N副作用,比如網(wǎng)絡(luò)請求、dispatch action 到 store 等等,都變成了 call、put 等 effect,由 task 部分控制執(zhí)行。那么具體怎么執(zhí)行的就可以隨意的切換了,這樣測試的時(shí)候只需要模擬傳入對應(yīng)的數(shù)據(jù),就可以測試 worker saga 了。
redux saga 設(shè)計(jì)成 generator 的形式是一種學(xué)習(xí)成本和可測試性的權(quán)衡。
還記得 redux-thunk 有啥問題么?多個(gè)異步過程之間的并行、串行的復(fù)雜關(guān)系沒法處理。那 redux-saga 是怎么解決的呢?
redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 來指定多個(gè)異步過程的關(guān)系:
比如 takeEvery 會對多個(gè) action 的每一個(gè)做同樣的處理,takeLatest 會對多個(gè) action 的最后一個(gè)做處理,race 會只返回最快的那個(gè)異步過程的結(jié)果,等等。
這些控制多個(gè)異步過程之間關(guān)系的 effect 正是 redux-thunk 所沒有的,也是復(fù)雜異步過程的管理必不可少的部分。
所以 redux-saga 可以做復(fù)雜異步過程的管理,而且具有很好的可測試性。
其實(shí)異步過程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 實(shí)現(xiàn)的,它也是一種復(fù)雜異步過程管理的方案。
redux-observable
redux-observable 用起來和 redux-saga 特別像,比如啟用插件的部分:
- const epicMiddleware = createEpicMiddleware();
- const store = createStore(
- rootReducer,
- applyMiddleware(epicMiddleware)
- );
- epicMiddleware.run(rootEpic);
和 redux saga 的啟動流程是一樣的,只是不叫 saga 而叫 epic。
但是對異步過程的處理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:
- import { ajax } from 'rxjs/ajax';
- const fetchUserEpic = (action$, state$) => action$.pipe(
- ofType('FETCH_USER'),
- mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
- map(response => ({
- type: 'FETCH_USER_FULFILLED',
- payload: response
- }))
- )
- );
通過 ofType 來指定監(jiān)聽的 action,處理結(jié)束返回 action 傳遞給 store。
相比 redux-saga 來說,redux-observable 支持的異步過程的處理更豐富,直接對接了 operator 的生態(tài),是開放的,而 redux-saga 則只是提供了內(nèi)置的幾個(gè) effect 來處理。
所以做特別復(fù)雜的異步流程處理的時(shí)候,redux-observable 能夠利用 rxjs 的操作符的優(yōu)勢會更明顯。
但是 redux-saga 的優(yōu)點(diǎn)還有基于 generator 的良好的可測試性,而且大多數(shù)場景下,redux-saga 提供的異步過程的處理能力就足夠了,所以相對來說,redux-saga 用的更多一些。
總結(jié)
前端框架實(shí)現(xiàn)了數(shù)據(jù)到視圖的綁定,我們只需要關(guān)心數(shù)據(jù)流就可以了。
相比 context 的混亂的數(shù)據(jù)流,redux 的 view -> action -> store -> view 的單向數(shù)據(jù)流更清晰且容易管理。
前端代碼中有很多異步過程,這些異步過程之間可能有串行、并行甚至更復(fù)雜的關(guān)系,放在組件里并不好管理,可以放在 redux 的中間件里。
redux 的中間件就是對 dispatch 的層層包裝,比如 redux-thunk 就是判斷了下 action 是 function 就執(zhí)行下,否則就是繼續(xù) dispatch。
redux-thunk 并沒有提供多個(gè)異步過程管理的機(jī)制,復(fù)雜異步過程的管理還是得用 redux-saga 或者 redux-observable。
redux-saga 透傳了 action 到 store,并且監(jiān)聽 action 執(zhí)行相應(yīng)的異步過程。異步過程的描述使用 generator 的形式,好處是可測試性。比如通過 take、takeEvery、takeLatest 來監(jiān)聽 action,然后執(zhí)行 worker saga。worker saga 可以用 put、call、fork 等 effect 來描述不同的副作用,由 task 負(fù)責(zé)執(zhí)行。
redux-observable 同樣監(jiān)聽了 action 執(zhí)行相應(yīng)的異步過程,但是是基于 rxjs 的 operator,相比 saga 來說,異步過程的管理功能更強(qiáng)大。
不管是 redux-saga 通過 generator 來組織異步過程,通過內(nèi)置 effect 來處理多個(gè)異步過程之間的關(guān)系,還是 redux-observable 通過 rxjs 的 operator 來組織異步過程和多個(gè)異步過程之間的關(guān)系。它們都解決了復(fù)雜異步過程的處理的問題,可以根據(jù)場景的復(fù)雜度靈活選用。