dva的思想還是很不錯的,大大提升了開發效率,dva集成了Redux以及Redux的中間件Redux-saga,以及React-router等等。得益於Redux的狀態管理,以及Redux-saga中通過Task和Effect來處理異步的概念,dva在這些工具的基礎上高度封裝,只暴露出幾個簡單的API就可以設計數據模型。
最近看了一下Redux-saga的源碼,結合以及之前在項目中一直採用的是redux-dark模式來將reducers和sagas(generator函數處理異步)拆分到不同的子頁面,每一個頁面中同一個文件中包含了該頁面狀態的reducer和saga,這種簡單的封裝已經可以大大的提升項目的可讀性。
最近看了dva源碼,熟悉了dva是在上層如何做封裝的。下面會從淺到深,淡淡在閱讀dva源碼過程中自己的理解。
- redux-dark模式
- dva 0.0.12版本的使用和源碼理解
本文的原文地址爲: https://github.com/forthealll...
歡迎star
一、redux-dark模式
在使用redux和redux-saga的時候,特別是如何存放reducer函數和saga的generator函數,這兩個函數是直接跟如何處理數據掛鉤的。
回顧一下,在redux中使用異步中間件redux-saga後,完整的數據和信息流向:
在存在異步的邏輯下,在UI Component中發出一個plain object的action,然後經過redux-saga這個中間件處理,redux-saga會將這個action傳入相應channel,通過redux-saga的effect方法(比如call、put、apply等方法)生成一個描述對象,然後將這個描述對象轉化成具有副作用的函數並執行。
在redux-saga執行具有副作用的函數時,又可以dispatch一個action,這個action也是一個plain object,會直接傳入到redux的reducer函數中進行處理,也就是說在redux-saga的task中發出的action,就是同步的action。
簡單的概括:從UI組件上發出的action經過了2層的處理,分別是redux-saga中間件和redux的reducer函數。
redux-dark模式很簡單,就是將同一個子頁面下的redux-saga處理action的saga函數,以及reducer處理該子頁面下的state的reducer函數,放在同一個文件中。
舉例來說:
import { connect } from 'react-redux';
class Hello extends React.Component{
componentDidMount(){
//發出一個action,獲取異步數據
this.props.dispatch({
type:'async_count'
})
}
}
export default connect({})(Hello);
從Hello組件中發出一個type = 'async_count'的action,我們用redux-dark模式來將saga和reducer函數放在同一個文件中:
import { takeEvery } from 'redux-saga/effects';
//saga
function * asyncCount(){
console.log('執行了saga異步...')
//發出一個原始的action
yield put({
type:'async'
});
}
function * helloSaga(){
//接受從UI組件發出的action
takeEvery('async_count',asyncCount);
}
//reducer
function helloReducer(state,action){
if(action.type === 'count');
return { ...state,count + 1}
}
上述就是一個將saga和reducer放在同一個文件裏面的例子。redux-dark模式來組織代碼,可以顯得比較直觀,統一了數據的處理層。分拆子頁面後,每一個子頁面對應一個文件。可讀性很高。
二、dva 0.0.12版本的使用和源碼理解
上述的redux-dark模式,就是一種簡單的處理,而dva的話是做了更近一步的封裝,dva不僅封裝了redux和redux-saga,還有react-router-redux、react-router等等。使得我們可以通過很簡單的配置,就能使用redux、redux-saga、react-router等。
下面首先以dva的初始版本爲例來理解一下dva的源碼。
(1)、dva 0.0.12的使用
來看官網給的使用dva 0.0.12的例子:
// 1. Initialize
const app = dva();
// 2. Model
app.model({
namespace: 'count',
state: 0,
effects: {
['count/add']: function*() {
console.log('count/add');
yield call(delay, 1000);
yield put({
type: 'count/minus',
});
},
},
reducers: {
['count/add' ](count) { return count + 1 },
['count/minus'](count) { return count - 1 },
},
subscriptions: [
function(dispatch) {
//..處理監聽等等函數
}
],
});
// 3. View
const App = connect(({ count }) => ({
count
}))(function(props) {
return (
<div>
<h2>{ props.count }</h2>
<button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
<button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
</div>
);
});
// 4. Router
app.router(({ history }) =>
<Router history={history}>
<Route path="/" component={App} />
</Router>
);
// 5. Start
app.start(document.getElementById('root'));
只要三步就完成了一個例子,如何處理action呢,我們以一個圖來表示:
也就是做UI組件上發出的對象類型的action,先去根據類型匹配=model初始化時候,effects屬性中的action type。
- 如果在effects的屬性中有相應的action type的處理函數,那麼先執行effects中的相應函數,在執行這個函數裏面可以二次發出action,二次發出的action會直接傳入到reducer函數中。
- 如果effects的屬性中沒有相應的action type的處理函數,那麼會直接從reducer中尋找有沒有相應類型的處理函數。
在dva初始化過程中的effects屬性中的函數,其實就是redux-saga中的saga函數,在該函數中處理直接的異步邏輯,並且該函數可以二次發出同步的action。
此外dva還可以通過router方法初始化路由等。
(2)、dva 0.0.12的源碼閱讀
下面來直接讀讀dva 0.0.12的源碼,下面的代碼是經過我精簡後的dva的源碼:
//Provider全局注入store
import { Provider } from 'react-redux';
//redux相關的api
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
//redux-saga相關的api,takeEvery和takeLatest監聽等等
import createSagaMiddleware, { takeEvery, takeLatest } from 'redux-saga';
//react-router相關的api
import { hashHistory, Router } from 'react-router';
//在react-router4.0之後已經較少使用,將路由的狀態存儲在store中
import { routerMiddleware, syncHistoryWithStore, routerReducer as routing } from 'react-router-redux';
//redux-actions的api,可以以函數式描述reducer等
import { handleActions } from 'redux-actions';
//redux-saga非阻塞調用effect
import { fork } from 'redux-saga/effects';
function dva() {
let _routes = null;
const _models = [];
//new dva暴露了3個方法
const app = {
model,
router,
start,
};
return app;
//添加models,一個model對象包含了effects,reducers,subscriptions監聽器等等
function model(model) {
_models.push(model);
}
//添加路由
function router(routes) {
_routes = routes;
}
function start(container) {
let sagas = {};
//routing是react-router-redux的routerReducer別名,用於擴展reducer,這樣以後擴展後的reducer就可以處理路由變化。
let reducers = {
routing
};
_models.forEach(model => {
//對於每一個model,提取其中的reducers和effects,其中reducers用於擴展redux的reducers函數,而effects用於擴展redx-saga的saga處理函數。
reducers[model.namespace] = handleActions(model.reducers || {}, model.state);
//擴展saga處理函數,sagas是包含了所有的saga處理函數的對象
sagas = { ...sagas, ...model.effects }; ---------------------------(1)
});
reducers = { ...reducers };
//獲取決定使用React-router中的那一個api
const _history = opts.history || hashHistory;
//初始化redux-saga
const sagaMiddleware = createSagaMiddleware();
//爲redux添加中間件,這裏添加了處理路由的中間件,以及redux-saga中間件。
const enhancer = compose(
applyMiddleware.apply(null, [ routerMiddleware(_history), sagaMiddleware ]),
window.devToolsExtension ? window.devToolsExtension() : f => f
);
const initialState = opts.initialState || {};
//通過combineReducers來擴展reducers,同時生成擴展後的store實例
const store = app.store = createStore(
combineReducers(reducers), initialState, enhancer
);
// 執行model中的監聽函數,監聽函數中傳入store.dispatch
_models.forEach(({ subscriptions }) => {
if (subscriptions) {
subscriptions.forEach(sub => {
store.dispatch, onErrorWrapper);
});
}
});
// 根據rootSaga來啓動saga,rootSaga就是redux-saga運行的主task
sagaMiddleware.run(rootSaga);
//創建history實例子,可以監聽store中的state的變化。
let history;
history = syncHistoryWithStore(_history, store); --------------------------------(2)
// Render and hmr.
if (container) {
render();
apply('onHmr')(render);
} else {
const Routes = _routes;
return () => (
<Provider store={store}>
<Routes history={history} />
</Provider>
);
}
function getWatcher(k, saga) {
let _saga = saga;
let _type = 'takeEvery';
if (Array.isArray(saga)) {
[ _saga, opts ] = saga;
_type = opts.type;
}
function* sagaWithErrorCatch(...arg) {
try {
yield _saga(...arg);
} catch (e) {
onError(e);
}
}
if (_type === 'watcher') {
return sagaWithErrorCatch;
} else if (_type === 'takeEvery') {
return function*() {
yield takeEvery(k, sagaWithErrorCatch);
};
} else {
return function*() {
yield takeLatest(k, sagaWithErrorCatch);
};
}
}
function* rootSaga() {
for (let k in sagas) {
if (sagas.hasOwnProperty(k)) {
const watcher = getWatcher(k, sagas[k]);
yield fork(watcher);
} -----------------------------(3)
}
}
function render(routes) {
const Routes = routes || _routes;
ReactDOM.render((
<Provider store={store}>
<Routes history={history} />
</Provider>
), container);
}
}
}
export default dva;
代碼的閱讀在上面都以注視的方式給出,值得注意的主要有一下3點:
- 在註釋(1)處, handleActions是通過redux-actions封裝後的一個API,用於簡化reducer函數的書寫。下面是一個handleActions的例子:
const reducer = handleActions(
{
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
},
{ counter: 0 }
);
INCREMENT和DECREMENT屬性的函數就可以分別處理,type = "INCREMENT"和type = "DECREMENT"的action。
- 在註釋 (2) 處,通過react-router-redux的api,syncHistoryWithStore可以擴展history,使得history可以監聽到store的變化。
- 在註釋(3)處是一個rootSaga, 是redux-saga運行的時候的主Task,在這個Task中我們這樣定義:
function* rootSaga() {
for (let k in sagas) {
if (sagas.hasOwnProperty(k)) {
const watcher = getWatcher(k, sagas[k]);
yield fork(watcher);
}
}
}
從全局的包含所有saga函數的sagas對象中,獲取相應的屬性,並fork相應的監聽,這裏的監聽常用的有takeEvery和takeLatest等兩個redux-saga的API等。
總結:上面就是dva最早版本的源碼,很簡潔的使用了redux、redux-saga、react-router、redux-actions、react-router-redux等.其目的也很簡單:
簡化redux相關生態的繁瑣邏輯