本文主要讲解redux的核心原理以及相关的周边技术原理。帮助你在使用的时候,知其所以然~
涉及:redux、redux异步解决方法、中间件、react-redux原理、Dva
用途
作为状态容器,提供对状态的查询、改变
进行管理。
从具体操作
-> 状态变更
-> 触发视图更新
,这一单向数据流的控制,不仅使得对状态变化的过程变得可控,同时解耦了数据M和视图V。
使用场景
1)应用复杂,将状态及页面逻辑reducer提取出来,利于代码结构的组织;
2)全局状态共享或可能被共享和改变的组件状态。
设计思想
1)web应用是一个状态机,视图和状态一一对应;
2)所有状态保存在一个对象内。
三大原则
单一数据源
state状态对象由store负责存储获取管理;
state为不可变对象(immutable data
),保证每次返回一个新的状态对象。
state只读
action是改变状态对象state的唯一方式;
action由dispatch函数触发,其描述了一种行为其常见形式;
{
type: 'actionType', //行为
payload: {} //需要传递的信息
}
payload是更新store数据的唯一来源。
reducer是纯函数
type Reducer = (state: S, action: A) => S
对action做出响应的响应,返回新的状态对象state(保证可回溯的原因),不应该产生副作用;
生成新的state有如下几种方式:
- Object.assign({}, oldState, newState);
- {...oldState, 更新值}
- Immutable
javascript中的基本类型:Boolean、Number、String、Null、undefined、Symbol
等都是不可变的(Immutable
),只有Object
是可变的(Mutable
).
store提供的方法:
subscribe
用于订阅事件,每次state变更后,都会触发其订阅的事件。
这里可以处理state -> props,以此更新视图组件的更新渲染(react-redux的connect就是这么实现的
);
dispatch
- 处理同步action
//该方法用于生成action
let actionCreator = (name) => ({ type: 'ADD_ITEM', payload: { name } });
//生成action
dispatch(actionCreator('M2'));
- 处理异步
1)redux-thunk
其通过扩展action,使得actionCreator返回一个function作为action。
let asyncActionCreator = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
//这里需要使用middleware支持异步,例如redux-thunk
var thunkMiddleware = function ({ dispatch, getState }) {
return function(next) {
return function (action) {
//如果是function,则传递dispatch、getState,并执行
return typeof action === 'function' ?
//原始的dispatch
action(dispatch, getState) :
next(action)
}
}
}
//使用1
store.dispatch(fetchPosts('xx'));
// 使用2:注意上面dispatch后返回的Promise,可以在dispatch异步数据,reducer处理后,做一些处理
store.dispatch(fetchPosts(genPromise)).then(() => console.log(store.getState()) );
thunk的应用:延迟执行、异步控制。作为Generator的next()的返回值,Thunk和Promise都是Generator自动执行的解决方案。
2)redux-sage
dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
redux-saga
是一个 redux 中间件;使用了 ES6 的 Generator 功能.
如何使用?
// 创建saga middleware
const sagaMiddleware = createSagaMiddleware();
// 注入saga middleware
applyMiddleware(sagaMiddleware);
//启动
sagaMiddleWare.run(rootSaga);
先来看rootSaga的写法:
// worker Saga : 将在 USER_FETCH_REQUESTED action 被 dispatch 时调用
function* workerSaga(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
/*
在每个 `USER_FETCH_REQUESTED` action 被 dispatch 时调用 fetchUser
允许并发(译注:即同时处理多个相同的 action)
*/
function* watcherSaga() {
yield takeEvery("USER_FETCH_REQUESTED", workerSaga);
}
export default watcherSaga;
由上例可知,rootSaga是Generator函数,而sagaMiddleWare.run();
是其自执行器;
启动后会执行takeEvery(),看下它做了什么事:
function* takeEvery("USER_FETCH_REQUESTED", workerSaga) {
//启动一个新的独立的task,执行一个构造的Generator
yield fork(function* () {
//注册挂起->消费注销,执行对应的saga->注册挂起...
while(true) {
//将当前actionType注册到发布中心,挂起,等待dispatch时在中间件中触发,匹配则执行该task的next()即workerSaga,同时注销该actionType的注册信息;
yield take("USER_FETCH_REQUESTED");
//执行saga任务
yield task(workerSaga);
}
});
}
注意这里的task()、take()、fork()
,包括saga中call()、put()
等返回的都是effect对象,比如call()执行后:
{
isEffect: true,
type: 'CALL',
fn: Api.fetchUser
}
task自执行器在根据next()的返回值,判断effect的类型做对应的操作,比如执行yield take('USER_FETCH_REQUESTED')
后,it.next()
返回的是:
{
type: 'take',
pattern: 'USER_FETCH_REQUESTED'
}
那么自执行器会判断:
//effect为take的执行逻辑
if (effect.type === 'take') {
runTakeEffect(result.value, next);
}
//runTakeEffect将当前Generator挂起,注册到channel中心,等待唤醒
function runTakeEffect(effect, cb) {
chan.take({cb, pattern:effect.pattern});
}
发布订阅中心channel的代码如下:
function channel() {
let _task = null;
//挂起task
function take(task) {
_task = task;
}
//dispatch时,中间件put触发对应的actionType,来匹配对应pattern的回调
function put(pattern, args) {
if(!_task) return;
if(pattern == _task.pattern) {
taker = null; //仅消费一次,注销,等待下次循环时再注册
_task.cb.call(null, args);//唤醒挂起task;调用next()继续控制Generator执行
}
}
return {
take,
put
}
}
最后来看下redux-saga中间件是到channel中匹配对应action的:
const sagaMiddleware = store => {
return next => action => {
next(action);
const { type, ...payload } = action;
//调用channel的put方法,会判断是否匹配,匹配的话则执行挂起的task,即对应的saga
channel.put(type, payload);
}
}
总结来说:
核心还是内部实现的Generator自执行器,对不同的命令对象做不同的处理逻辑;
循环过程:启动时,take挂起,等待put唤醒挂起的task调用next(),同时注销effect;让task自执行对应的gennerator函数。执行完后循环,再重新注册
发布订阅模式:chanel发布订阅中心,run执行时首先执行takeEvery,注册effect。在中间件中触发put(action)
问题:
1.为什么不用Async作为自执行器呢?
redux-saga内部实现了Generator自执行器,可以自主的控制流程的暂停,运行等;
2.为什么call()返回effect对象呢?
一个是根据effect类型可以做多种操作;再者这也是其比redux-thunk更利于测试的原理。
redux-sage相较于redux-thunk的优势:
- 不改动action;
-
Generator同步的写法,实现异步流程的控制管理;
注意:
let res = yield call('xx');
, res的值为next(val)传入的参数,而不是yield后面的表达式!
Generator通过yield
和next
来传递数据来控制函数的内部流程(内外部的双向通信
)。 - 单独的文件组织,以及yield后call等关键字返回的是effect对象,便于测试维护;
中间件
洋葱圈模型,和KOA的中间件原理类似;
在action真正处理前,赋予诸如日志记录等能力。
//将中间件绑定到
let applyMiddleware = middlewares => (createStore) => (...args) => {
let store = createStore(...args);
let dispatch = store.dispatch;
let params = {
dispatch,
getState: store.getState
};
middlewares = middlewares.map(mw => mw(params));
//组合中间件,增强dispatch
//装饰者模式 & 洋葱圈,compose对中间件的组合
dispatch = compose(middlewares)(dispatch);
//返回增强后的dispatch
return Object.assign(store, {dispatch});
}
React-Redux
基于容器组件和展示组件相分离的开发思想,将组件分为:UI组件
和容器组件
.
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList;
mapStateToProps:将state映射到展示组件的props;
mapDispatchToProps: 定义了组件内部需要的事件,内部注入了dispatch;
怎么获取state?
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。一种方式是把它以 props 的形式传入到所有容器组件中,存在层层繁琐的传递,而且往往中间组件并不需要的问题。建议的方式是使用指定的 React Redux 组件
:
生产者
:组件
//使用
//Provider利用了React的context
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}
消费者
:connect
最后看下connect的简单实现:
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super();
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {}
let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return
}
}
return Connect
}
Dva
Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:
- 把 store 及 saga 统一为一个
model
的概念, 写在一个 js 文件里面; - 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作;
- model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️
更多技术分享,欢迎【扫码关注】~
参考:
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...
https://blog.csdn.net/TurkeyC...
https://dvajs.com/guide/fig-s...