Redux使用中的几个点:javascript
在传统的 MVC 架构中,咱们能够根据须要建立无数个 Model,而 Model 之间能够互相监听、触发事件甚至循环或嵌套触发事件,这些在 Redux 中都是不容许的。由于在 Redux 的思想里,一个应用永远只有惟一的数据源。
实际上,使用单一数据源的好处在于整个应用状态都保存在一个对象中,这样咱们随时能够提取出整个应用的状态进行持久化(好比实现一个针对整个应用的即时保存功能)。此外,这样的设计也为服务端渲染提供了可能。java
在 Redux 中,咱们并不会本身用代码来定义一个 store。取而代之的是,咱们定义一个 reducer,它的功能是根据当前触发的 action 对当前应用的状态(state)进行迭代,这里咱们并无直接修改应用的状态,而是返回了一份全新的状态。react
Redux 提供的 createStore 方法会根据 reducer 生成 store。最后,咱们能够利用 store. dispatch
方法来达到修改状态的目的。编程
在 Redux 里,咱们经过定义 reducer 来肯定状态的修改,而每个 reducer 都是纯函数,这意味着它没有反作用,即接受必定的输入,一定会获得必定的输出。redux
这样设计的好处不只在于 reducer 里对状态的修改变得简单、纯粹、可测试,更有意思的是,Redux 利用每次新返回的状态生成酷炫的时间旅行(time travel)调试方式,让跟踪每一次由于触发 action 而改变状态的结果成为了可能。api
咱们从store的诞生开始提及。create store函数API文档以下:数组
createStore(reducer, [initialState], enhancer)
能够看出,它接受三个参数:reducer、initialState 和 enhancer 。Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 类似,它也容许你经过复合函数改变 store 接口。缓存
再来看看他的返回值:闭包
{ dispatch: f (action), getState: f (), replaceReducer: f (nextReducer), subscribe: f (listener), Symbol(observable): f () }
store的返回值就是一个普通对象,里面有几个经常使用的方法:架构
这里挑几个方法介绍:
在完成基本的参数校验以后,在 createStore 中声明以下变量及 getState 方法:
var currentReducer = reducer var currentState = initialState var listeners = [] // 当前监听 store 变化的监听器 var isDispatching = false // 某个 action 是否处于分发的处理过程当中 /** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */ function getState() { return currentState }
getState方法就是简单返回当前state,若是state没有被reducer处理过,他就是initialState。
在 getState 以后,定义了 store 的另外一个方法 subscribe:
function subscribe(listener) { listeners.push(listener) var isSubscribed = true return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false var index = listeners.indexOf(listener) listeners.splice(index, 1) } }
Store 容许使用store.subscribe
方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
显然,只要把 View 的更新函数(对于 React 项目,就是组件的render
方法或setState
方法)放入listen
,就会实现 View 的自动渲染。你可能会感到奇怪,好像咱们在 Redux 应用中并无使用 store.subscribe 方法?事实上,
React Redux 中的 connect 方法隐式地帮咱们完成了这个工做。
store.subscribe
方法返回一个函数,调用这个函数就能够解除监听。
dispatch是redux的核心方法:
function dispatch(action) { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } listeners.slice().forEach(listener => listener()) return action }
判断当前是否处于某个 action 的分发过程当中,这个检查主要是为了不在 reducer 中分发 action 的状况,由于这样作可能致使分发死循环,同时也增长了数据流动的复杂度。
确认当前不属于分发过程当中后,先设定标志位,而后将当前的状态和 action 传给当前的reducer,用于生成最新的 state。这看起来一点都不复杂,这也是咱们反复强调的 reducer 工做过程——纯函数、接受状态和 action 做为参数,返回一个新的状态。
在获得新的状态后,依次调用全部的监听器,通知状态的变动。须要注意的是,咱们在通知监听器变动发生时,并无将最新的状态做为参数传递给这些监听器。这是由于在监听器中,咱们能够直接调用 store.getState() 方法拿到最新的状态。
最终,处理以后的 action 会被 dispatch 方法返回。
function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.'); } currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); }
这是为了拿到全部 reducer 中的初始状态(你是否还记得在定义 reducer 时,第一个参数为previousState,若是该参数为空,咱们提供默认的 initialState)。只有全部的初始状态都成功获取后,Redux 应用才能有条不紊地开始运做。
It provides a third-party extension point between dispatching an action, and the moment it reaches
the reducer
它提供了一个分类处理 action 的机会。在middleware 中,你能够检阅每个流过的 action,挑选出特定类型的action 进行相应操做,给你一次改变 action 的机会。
常规的同步数据流模式的流程图以下:
不一样业务需求下,好比执行action以前和以后都要打log;action触发一个异步的请求,请求回来以后渲染view等。须要为这一类的action添加公共的方法或者处理,使用redux middleware流程图以下:
每个 middleware 处理一个相对独立的业务需求,经过串联不一样的 middleware 实现变化多样的功能。好比上面的业务,咱们把处理log的代码封装成一个middleware,处理异步的也是一个middleware,二者串联,却又相互独立。
使用middleware以后,action触发的dispatch并非原来的dispatch,而是通过封装的new dispatch,在这个new dispatch中,按照顺序依次执行每一个middleware,最后调用原生的dispatch。
咱们来看下logger middleware如何实现的:
export default store => next => action => { console.log('dispatch:', action); next(action); console.log('finish:', action); }
这里代码十分简洁,就是在next调用下一个middleware以前和以后,分别打印两次。
Redux 提供了 applyMiddleware 方法来加载 middleware,该方法的源码以下:
import compose from './compose'; export default function applyMiddleware(...middlewares) { return function (next) { return function (reducer, initialState) { let store = next(reducer, initialState); let dispatch = store.dispatch; let chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action), }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch, }; } } }
其中compose源码以下:
function compose(...funcs) { return arg => funcs.reduceRight((composed, f) => f(composed), arg); }
使用的时候,以下:
const newStore = applyMiddleware([mid1, mid2, mid3, ...])(createStore)(reducer, initialState);
ok,相关源码已就位,咱们来详细解析一波。
函数式编程思想设计 :middleware 的设计有点特殊,是一个层层包裹的匿名函数,这实际上是函数式编程中的
currying,它是一种使用匿名单参数函数来实现多参数函数的方法。applyMiddleware 会对 logger 这个middleware 进行层层调用,动态地将 store 和 next 参数赋值。currying 的 middleware 结构的好处主要有如下两点。
给 middleware 分发 store:newStore建立完成以后,applyMiddleware 方法陆续得到了3个参数,第一个是 middlewares 数组[mid1, mid2, mid3, ...],第二个是 Redux 原生的 createStore ,最后一个是 reducer。而后,咱们能够看到 applyMiddleware 利用 createStore 和 reducer 建立了一个 store。而 store 的 getState方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量 store:
const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action), }; chain = middlewares.map(middleware => middleware(middlewareAPI));
而后,让每一个 middleware 带着 middlewareAPI 这个参数分别执行一遍。执行完后,得到 chain数组 [f1, f2, ... , fx, ..., fn],它保存的对象是第二个箭头函数返回的匿名函数。由于是闭包,每一个匿名函数均可以访问相同的 store,即 middlewareAPI。
middlewareAPI 中的 dispatch 为何要用匿名函数包裹呢?咱们用 applyMiddleware 是为了改造 dispatch,因此 applyMiddleware 执行完后,dispatch 是变化了的,而 middlewareAPI 是 applyMiddleware 执行中分发到各个 middleware 的,因此必须用匿名函数包裹 dispatch,这样只要 dispatch 更新了,middlewareAPI 中的 dispatch 应用也会发生变化。
组合串联 middleware:这一层只有一行代码,倒是 applyMiddleware 精华之所在dispatch = compose(...chain)(store.dispatch);
,其中 compose 是函数式编程中的组合,它将 chain 中的全部匿名函数 [f1, f2, ... , fx, ..., fn]组装成一个新的函数,即新的 dispatch。当新 dispatch 执行时,[f1, f2, ... , fx, ..., fn],从右到左依次执行。
compose(...funcs) 返回的是一个匿名函数,其中 funcs 就是 chain 数组。当调用 reduceRight时,依次从 funcs 数组的右端取一个函数 fx 拿来执行,fx 的参数 composed 就是前一次 fx+1 执行的结果,而第一次执行的 fn(n 表明 chain 的长度)的参数 arg 就是 store.dispatch。因此,当 compose 执行完后,咱们获得的 dispatch 是这样的,假设 n = 3:
dispatch = f1(f2(f3(store.dispatch))));
这时调用新 dispatch,每个 middleware 就依次执行了。
在 middleware 中调用 dispatch 会发生什么:通过 compose 后,全部的 middleware 算是串联起来了。但是还有一个问题,在分发 store 时,咱们提到过每一个 middleware 均可以访问 store,即 middlewareAPI 这个变量,也能够拿到 store 的dispatch 属性。那么,在 middleware 中调用 store.dispatch() 会发生什么,和调用 next() 有区别吗?如今咱们来讲明二者的不一样:
const logger = store => next => action => { console.log('dispatch:', action); next(action); console.log('finish:', action); }; const logger = store => next => action => { console.log('dispatch:', action); store.dispatch(action); console.log('finish:', action); };
在分发 store 时咱们解释过,middleware 中 store 的 dispatch 经过匿名函数的方式和最终compose 结束后的新 dispatch 保持一致,因此,在 middleware 中调用 store.dispatch() 和在其余任何地方调用的效果同样。而在 middleware 中调用 next(),效果是进入下一个 middleware,下图就是redux middleware最著名的洋葱模型图。
若是一个项目过大,咱们一般按模块来写reducer,可是redux create store只接受一个reducer参数,因此咱们须要合并reducer。这里就用到了redux提供的combineReducer
辅助函数:
combineReducers({ layout, home, ...asyncReducers })
这个函数用起来很简单,就是传入一个对象,key是模块reducer对应的名字, 值是对应reducer。值是一个function,至关因而一个新的reducer,源码以下:
export default function combineReducers(reducers) { var reducerKeys = Object.keys(reducers) var finalReducers = {} for (var i = 0; i < reducerKeys.length; i++) { var key = reducerKeys[i] if (process.env.NODE_ENV !== 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } var finalReducerKeys = Object.keys(finalReducers) if (process.env.NODE_ENV !== 'production') { var unexpectedKeyCache = {} } var sanityError try { assertReducerSanity(finalReducers) } catch (e) { sanityError = e } return function combination(state = {}, action) { if (sanityError) { throw sanityError } if (process.env.NODE_ENV !== 'production') { var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache) if (warningMessage) { warning(warningMessage) } } var hasChanged = false var nextState = {} for (var i = 0; i < finalReducerKeys.length; i++) { var key = finalReducerKeys[i] var reducer = finalReducers[key] var previousStateForKey = state[key] var nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state } }
源码不是不少,除去一些验证代码,剩下的就是说:return一个function,咱们暂时称呼他combination,就至关因而与一个总的reducer,每次action都会走到combination中,combination会遍历输入的reducer,将action放到每一个reducer中执行一下,计算出返回结果就是nextState,nextState于previousState若是!==说明改变了,返回nextState,不然返回执行以前的state。
这也解释了不一样模块actionType若是相同的话,两个模块的reducer都会走一遍的问题,在actionType名称前面加上模块前缀便可解决问题。
Provider与Connet组件都是React-Redux提供的核心组件,二者看起来功能同样,都是帮助容器组件获取store中的数据,可是原理与功能却不一样。
Provider组件在全部组件的最外层,其接受store做为参数,将store里的state使用context属性向下传递。部分源码:
export default class Provider extends Component { getChildContext() { return { store: this.store } } constructor(props, context) { super(props, context) this.store = props.store } render() { const { children } = this.props return Children.only(children) } }
利用context这个属性,Provider全部子组件都可以拿到这个属性。
connect实现的功能是将须要关联store的组件和store的dispatch等数据混合到一块,这块就是一个高阶组件典型的应用:
import hoistStatics from 'hoist-non-react-statics' export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { // ... return function wrapWithConnect(WrappedComponent) { // ... class Connect extends Component { // ... render() { // ... if (withRef) { this.renderedElement = createElement(WrappedComponent, { ...this.mergedProps, ref: 'wrappedInstance' }) } else { this.renderedElement = createElement(WrappedComponent, this.mergedProps ) } return this.renderedElement } } // ... return hoistStatcis(Connect, WrappedComponent); } }
仍是先从他的四个参数提及:
connect 的第一个参数定义了咱们须要从 Redux 状态树中提取哪些部分看成 props 传给当前组件。通常来讲,这也是咱们使用 connect 时常常传入的参数。事实上,若是不传入这个参数,React 组件将永远不会和 Redux 的状态树产生任何关系。具体在源代码中的表现为:
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { const shouldSubscribe = Boolean(mapStateToProps) // ... class Connect extends Component { // ... trySubscribe() { if (shouldSubscribe && !this.unsubscribe) { this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) this.handleChange() } } // ... } }
mapStateToProps
会订阅 Store,每当state
更新的时候,就会自动执行,从新计算 UI 组件的参数,从而触发 UI 组件的从新渲染。
mapStateToProps
的第一个参数老是state
对象,还可使用第二个参数,表明容器组件的props
对象。
这块的源码相对较简单:
const mapState = mapStateToProps || defaultMapStateToProps class Connect extends Component { computeStateProps(store, props) { if (!this.finalMapStateToProps) { return this.configureFinalMapState(store, props) } const state = store.getState() const stateProps = this.doStatePropsDependOnOwnProps ? this.finalMapStateToProps(state, props) : this.finalMapStateToProps(state) if (process.env.NODE_ENV !== 'production') { checkStateShape(stateProps, 'mapStateToProps') } return stateProps } configureFinalMapState(store, props) { const mappedState = mapState(store.getState(), props) const isFactory = typeof mappedState === 'function' this.finalMapStateToProps = isFactory ? mappedState : mapState this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1 if (isFactory) { return this.computeStateProps(store, props) } if (process.env.NODE_ENV !== 'production') { checkStateShape(mappedState, 'mapStateToProps') } return mappedState } }
这块原理很简单,进行一些参数校验,判断第一个参数mapStateToProps返回值是否为function,若是是递归调用,不是的话算出返回值。若是没传这个参数,默认给{}。
咱们可能会疑惑为何传给 connect 的第一个参数自己是一个函数,react-redux 还容许这个函数的返回值也是一个函数呢?
简单地说,这样设计能够容许咱们在 connect 的第一个参数里利用函数闭包进行一些复杂计算的缓存,从而实现效率优化的目的
当咱们使用的时候:
const mapStateToProps = (state, props) => ({ home: state.home, layout: state.layout });
使用ownProps
做为参数后,若是容器组件的参数发生变化,也会引起 UI 组件从新渲染
人如其名,它接受 store 的 dispatch 做为第一个参数,同时接受 this.props 做为可选的第二个参数。利用这个方法,咱们能够在 connect 中方便地将 actionCreator 与 dispatch 绑定在一块儿(利用 bindActionCreators 方法),最终绑定好的方法也会做为 props 传给当前组件。这块的源码与mapStateToProps同样,就不贴了。
bindActionCreator
function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)) }
前两个参数返回的对象,都要跟组件自身的props merge一下,造成一个新的对象赋值给对应组件,咱们能够在这一步作一些处理,这个参数就是干这个的,该参数签名:
mergeProps(stateProps, dispatchProps, ownProps): props
默认状况若是没传该参数,返回Object.assign(ownProps, stateProps, dispatchProps)
。
若是指定这个参数,能够定制 connector 的行为。
pure = true
] (Boolean): 若是为 true,connector 将执行 shouldComponentUpdate
而且浅对比 mergeProps
的结果,避免没必要要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。 withRef = false
] (Boolean): 若是为 true,connector 会保存一个对被包装组件实例的引用,该引用经过 getWrappedInstance()
方法得到。默认值为 false。 这个connect组件还干了一件事,状态缓存判断。当store变了的时候,先后状态判断,若是状态不等,更新组件,而且完成事件分发。
上面讲了大量的函数源码,这么些函数之间的关系:
初始化阶段:
更新数据阶段:
redux核心函数大量使用了匿名函数和闭包来实现数据共享和状态同步。
使用函数柯里化s实现参数复用,本质上是下降通用性,提升适用性。
对于state这种核心状态使用getState()计算出新的state,而不是直接返回一个state对象。
使用观察者订阅者模式实现数据响应。
平时开发不常接触的api实现Provider与Connect通讯。