前言
我开通了一个微信公共号“王和阳的航海日志”,在上面记录着自己的学习、思考、实践和成长的过程,欢迎关注、交流和拍砖。
最近在项目里用了DVA,一个基于Redux的前端应用开发框架,用dva能让我们省去配置项目的一堆麻烦事儿。关于Dva的使用和介绍这里就不多说了,官方文档已经讲得很详细了。下面简单结合我自己的实践经历,讲讲DVA的一些思想以及一些关于数据流向的想法。
Dva的框架和由来
一图胜前言,首先我们看下传统的React项目的组件结构是怎么样的:
如果<TodoList/>
和<AddTodoBtn/>
想要发生联系,则只能通过父组件<App/>
来实现,这个方式在页面比较简单的时候还能够胜任,但若项目复杂起来之后,整个页面的数据流向就会变得如下图所示:
在这种情况下对项目进行维护绝对是个灾难,同时对这样的项目进行改动也是很困难的事儿,因为不同组件之间耦合的地方太多了,所以我们需要对state
做额外的管理,于是我们就使用了Redux
,那么项目结构就变成了这样:
可以看到这已经把state
和处理逻辑从 里面抽取出来了, 原先的add
和finish
操作也变成了reducer
里的函数。因为<TodoList/>
及<AddTodoBtn/>
都是 Pure Component, 那么通过 connect 方法使这些组件建立起与 store
的联系,同时通过 dispatch
向 store 发送 action
, 促发 store
的状态进行变化, 一旦状态有变, 因为组件和store
是被 connect,那么组件也就会随之更新,而且这个过程是可以被拦截的,所以我们就可以很方便地增加各种 Middleware
,从而实现各种自定义功能, 例如log、高阶组件,并且这样的结构耦合度更低, 复用度更高, 扩展性更好。
需要注意的是,上面的项目结构其实是不包含异步处理的,则为了处理异步请求,我们引入了redux-sage,redux-saga
会拦截异步action并发起http请求,以发送type=add的异步action
网络请求来说,若请求成功,则继续发送一个type=addSuccess的action
,若请求失败,则发送一个type=addFail的action
。
在了解了上面这些概念后,使用了dva
后项目的结构也就呼之欲出了:
dva
本身就是React + Redux + saga
的组合,而图中红框的部分就是dva
,可以很清楚地看到,dva
把store
和saga
结合成了一个model,同时增加了一个subscription
来订阅其他来源的action
(例如键盘操作、路由变化)
下面简单总结下dva中的一些概念以及自己的一些思考
state
state
表示 Model
的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值),且需要把这个state当做不可变数据(immutable data)来处理,保证每次得到的state都是全新对象,新旧对象间不存在引用关系,在检测两个state
里的复杂对象时只需要比较其属性的引用即可,这样做的好处是能够提升性能,便于测试和追踪变化(可用来实现神奇又好玩的时光机穿梭神器 redux-devtool)。具体到代码里就如下图所示:
state: {
keyword: '',
moduleList: [],
},
.....
return { ...state, keyword };
action 和 dispatch
action
是改变 state
的唯一途径,是一个普通的 javascript 对象,它描述了一个行为且是改变 state
的唯一途径。从用户UI操作事件、网络请求回调和 WebSocket 等其他地方获得的数据,最终都会通过 dispatch
函数调用一个 action
,从而改变对应的数据。action
必须带有 type
指明具体的行为名称,且能附带上额外的信息。
dispatching
是一个用于触发 action
的函数,且 dispatch
是在组件 connect
Models以后,才能通过 props 传入。action
和dispath
存在的意义就在于将“如何改变数据”和“改变数据”这两件事分开进行,让我们对如何改变state
有更清晰的思路。
dispatch({
type: 'add',
payload:{
item:'a'
}
});
reducer
reducer
是描述如何改变数据的,它接受两个参数(之前已经累积运算的结果和当前要被累积的值),并返回一个新的累积结果。
通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,即同样的输入必然得到同样的输出,它们不应该产生任何副作用,或者用更加专业的话说,这个函数是幂等的。
effect
effect
被称为副作用,在我们的应用中,最常见的就是异步操作(例如网络请求)。它来自于函数式编程的概念,之所以叫副作用是因为同样的输入不一定获得同样的输出。现在函数式编程大行其道的原因主要有以下两点:
- 函数式编程是面向数学的抽象,更加符合人的思维逻辑
- 函数是引用透明且没有副作用,不依赖外部的状态也不改变外部的状态。
- 因为函数是不可变的,则由于多个线程之间不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地进行并发,尤其是在对称多处理器(SMP)架构下能够更好地利用多个核提供的并行处理能力。要知道现在计算机的计算能力的增加已经不是依靠主频频率的提升,而是更依赖于CPU核心个数的增加,这一点带来的好处是母庸置疑的5。
(如果你想了解更多关于函数式编程的信息,可以阅读JS函数式编程指南。)
dva 为了控制副作用的操作,引入了redux-sagas做异步流程控制,且因为采用了generator的相关概念,所以能将异步转成同步写法,从而将effects转为纯函数。
effects: {
* createModule({ payload: { formData} }, { call, put }) {
const result = yield call(Services.createModule, formData);
if (result) {
yield put({ type: 'startSearch'});
}
}
}
subscription
subscriptions
是一种从 源 获取数据的方法,它来自于 elm。
Subscription 用于订阅一个数据源,然后根据条件 dispatch
需要的action
。数据源可以是服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
app.model({
namespace: 'count',
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) => {
// 第1次进入页面,请求模块数据
if (pathname === '/module') {
dispatch({ type: 'startSearch'});
}
});
},
}
});
简短的完整代码
import dva, { connect } from 'dva';
import { Router, Route } from 'dva/router';
import React from 'react';
import styles from './index.less';
import key from 'keymaster';
const app = dva();
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
},
});
const CountApp = ({count, dispatch}) => {
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>
</div>
</div>
);
};
function mapStateToProps(state) {
return { count: state.count };
}
const HomePage = connect(mapStateToProps)(CountApp);
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
app.start('#root');
// ---------
// Helpers
function delay(timeout){
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
}