共计 7049 个字符,预计需要花费 18 分钟才能阅读完成。
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 相关生态的繁琐逻辑
参考源码地址:https://github.com/dvajs/dva/…