react-redux追索 不管如何,react-redux是在redux的基础上的封装。
react-redux 这里确定一下版本号 这里版本号是v5.1.1 。后面版本有一些变化,我们后面再看看。
react-redux大致上和router一样 也是走的HOC(高阶组件) && context的线路。不过这里还是简单了解一下。
1 2 3 4 5 6 7 8 9 10 11 12 const store = createStore(rootReducer)ReactDOM.render( <Provider store={store}> <Router history={history}> <Route path="/" component={App}> <Route path="foo" component={Foo}/> <Route path="bar" component={Bar}/> </Route> </Router> </Provider>, document .getElementById('root' ) )
这里Provider
必须和connect
配合使用。
TODO例子
Provider组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import { Component, Children } from 'react' import PropTypes from 'prop-types' import { storeShape, subscriptionShape } from '../utils/PropTypes' import warning from '../utils/warning' let didWarnAboutReceivingStore = false function warnAboutReceivingStore ( ) { if (didWarnAboutReceivingStore) { return } didWarnAboutReceivingStore = true } export function createProvider (storeKey = 'store' ) { const subscriptionKey = `${storeKey} Subscription` class Provider extends Component { getChildContext ( ) { return { [storeKey]: this [storeKey], [subscriptionKey]: null } } constructor (props, context ) { super (props, context) this [storeKey] = props.store; } render ( ) { return Children.only(this .props.children) } } Provider.propTypes = { store: storeShape.isRequired, children: PropTypes.element.isRequired, } Provider.childContextTypes = { [storeKey]: storeShape.isRequired, [subscriptionKey]: subscriptionShape, } return Provider } export default createProvider()
这里的核心还是context相关的代码——getChildContext
&& childContextTypes
。这里Provider导出的是createProvider()
。返回的就是一个初始化了的,带有名为store的context的组件。
connect 项目使用简况 看看connect在一个react-redux项目中是怎么使用的,常见的就是在Container(页面级别)对组件进行一重包装
1 2 3 4 5 6 7 8 9 @connect( (state: RootState, ownProps): any => { return { dashBoard : state.dashBoard }; }, (dispatch: Dispatch): any => ({ actions: bindActionCreators(omit(dashBoardActions, 'Type' ), dispatch), }), ) export default class DashBoard extends React .Component <DashBoard .Props > {}
经过包装后的组件,就可以在组件内部引用到this.props.dashBoard属性,并且可以调用this.props.actions.xxxActions。
可以看出connect这个注解的作用,主要是第一个函数(mapStateToProps)给props挂载了函数一返回的属性,第二个函数(mapDispatchToProps)这挂载了相关的actions到props上。
源码剖析 下面我们看看connect函数内容。
connect
最后返回的实质上是一个HOC。
createConnect 通过这个HOC方法,监听reduxStore,然后把下级组件需要的state(通过mapStateToProps获取)和action creator(通过mapDispatchToProps)。并绑定到wrappedComponent的props。
这一块的代码比较多,所以仅仅遴选最关键的逻辑节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export function createConnect ({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {} ) { return function connect ( mapStateToProps, mapDispatchToProps, mergeProps, { pure = true , areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ...extraOptions } = {} ) { const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps' ) const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps' ) const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps' ) return connectHOC(selectorFactory, { methodName: 'connect' , getDisplayName: name => `Connect(${name} )` , shouldHandleStateChanges: Boolean (mapStateToProps), initMapStateToProps, initMapDispatchToProps, initMergeProps, pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, }) } } export default createConnect()
所以默认导出的就是return function connect(...){...}
这一块的代码。
connectAdvanced connect的操作实质指向了connectAdvanced。其代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 export default function connectAdvanced ( selectorFactory, { getDisplayName = name => `ConnectAdvanced(${name} )` , methodName = 'connectAdvanced' , renderCountProp = undefined , shouldHandleStateChanges = true , storeKey = 'store' , withRef = false , ...connectOptions } = {} ) { const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ const contextTypes = { [storeKey]: storeShape, [subscriptionKey]: subscriptionShape, } const childContextTypes = { [subscriptionKey]: subscriptionShape, } return function wrapWithConnect (WrappedComponent ) { const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component' const displayName = getDisplayName(wrappedComponentName) const selectorFactoryOptions = { ...connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, withRef, displayName, wrappedComponentName, WrappedComponent } class Connect extends Component { } Connect.WrappedComponent = WrappedComponent Connect.displayName = displayName Connect.childContextTypes = childContextTypes Connect.contextTypes = contextTypes Connect.propTypes = contextTypes return hoistStatics(Connect, WrappedComponent) } }
而经过@connect注解后的组件,实质上是调用了connectAdvanced中返回的wrapWithConnect对组件进行了操作。
wrapWithConnect hoistStatics因为涉及最终返回结果的包装处理,所以必须首先看看。这个hoistStatics实质上是解决HOC中一个缺陷而诞生的 。它的作用是将被包装组件的静态属性、方法等非react属性放到HOC上去,这样被包装组件上的静态属性在HOC上也就可以正常访问了。
这里hoistStatics函数是将WrappedComponent上帝静态属性放到Connect组件上并返回修改后的Connect组件。这里回忆一下我们写React页面时候很多时候会设定静态的属性就很容易理解了。
Connect组件 截下来是研究一下关键的Connect组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 class Connect extends Component { constructor (props, context ) { super (props, context) this .version = version this .state = {} this .renderCount = 0 this .store = props[storeKey] || context[storeKey] this .propsMode = Boolean (props[storeKey]) this .setWrappedInstance = this .setWrappedInstance.bind(this ) invariant(this .store, `Could not find "${storeKey} " in either the context or props of ` + `"${displayName} ". Either wrap the root component in a <Provider>, ` + `or explicitly pass "${storeKey} " as a prop to "${displayName} ".` ) this .initSelector() this .initSubscription() } getChildContext ( ) { const subscription = this .propsMode ? null : this .subscription return { [subscriptionKey]: subscription || this .context[subscriptionKey] } } componentDidMount ( ) { if (!shouldHandleStateChanges) return this .subscription.trySubscribe() this .selector.run(this .props) if (this .selector.shouldComponentUpdate) this .forceUpdate() } componentWillReceiveProps (nextProps ) { this .selector.run(nextProps) } shouldComponentUpdate ( ) { return this .selector.shouldComponentUpdate } componentWillUnmount ( ) { if (this .subscription) this .subscription.tryUnsubscribe() this .subscription = null this .notifyNestedSubs = noop this .store = null this .selector.run = noop this .selector.shouldComponentUpdate = false } getWrappedInstance ( ) { invariant(withRef, `To access the wrapped instance, you need to specify ` + `{ withRef: true } in the options argument of the ${methodName} () call.` ) return this .wrappedInstance } setWrappedInstance (ref ) { this .wrappedInstance = ref } initSelector ( ) { const sourceSelector = selectorFactory(this .store.dispatch, selectorFactoryOptions) this .selector = makeSelectorStateful(sourceSelector, this .store) this .selector.run(this .props) } initSubscription ( ) { if (!shouldHandleStateChanges) return const parentSub = (this .propsMode ? this .props : this .context)[subscriptionKey] this .subscription = new Subscription(this .store, parentSub, this .onStateChange.bind(this )) this .notifyNestedSubs = this .subscription.notifyNestedSubs.bind(this .subscription) } onStateChange ( ) { this .selector.run(this .props) if (!this .selector.shouldComponentUpdate) { this .notifyNestedSubs() } else { this .componentDidUpdate = this .notifyNestedSubsOnComponentDidUpdate this .setState(dummyState) } } notifyNestedSubsOnComponentDidUpdate ( ) { this .componentDidUpdate = undefined this .notifyNestedSubs() } isSubscribed ( ) { return Boolean (this .subscription) && this .subscription.isSubscribed() } addExtraProps (props ) { if (!withRef && !renderCountProp && !(this .propsMode && this .subscription)) return props const withExtras = { ...props } if (withRef) withExtras.ref = this .setWrappedInstance if (renderCountProp) withExtras[renderCountProp] = this .renderCount++ if (this .propsMode && this .subscription) withExtras[subscriptionKey] = this .subscription return withExtras } render ( ) { const selector = this .selector selector.shouldComponentUpdate = false if (selector.error) { throw selector.error } else { return createElement(WrappedComponent, this .addExtraProps(selector.props)) } } }
查看完它的源代码可以很容易知道,这个组件本质上呢,是将WrappedComponent加入了一些新的props属性后作为一个children的HOC,所以相关更新逻辑,都在Connect里面。
initSelector && redux回顾 关于initSelector这个,必须关联redux来看。当我们进行订阅时候实质上是在依赖redux进行订阅,而react-redux这里的订阅只是基于此的包装。我们在Provider组件中传入的store这个props,它的初始值由redux包里面的createStore函数而来。
关于这个函数,可以看看它最后的return部分和ReadMe里面的最简用例:
1 2 3 4 5 6 7 return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { createStore } from 'redux' function counter (state = 0 , action ) { switch (action.type) { case 'INCREMENT' : return state + 1 case 'DECREMENT' : return state - 1 default : return state } } let store = createStore(counter)store.subscribe(() => console .log(store.getState())) store.dispatch({ type : 'INCREMENT' }) store.dispatch({ type : 'INCREMENT' }) store.dispatch({ type : 'DECREMENT' })
initSelector这个函数中selectorFactory函数指向selectorFactory.js中的finalPropsSelectorFactory。
当我们在页面内运行this.props.dispatch实际上是在执行redux上的dispatch, 那么可以看看这个dispatch是如何挂载到HOC的props上的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export default function finalPropsSelectorFactory (dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) { const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options) if (process.env.NODE_ENV !== 'production' ) { verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps, options.displayName) } const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory return selectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options ) }
结合initSelector的代码,可以相对容易理解这一点。
initSubscription 关于initSubscription函数呢,他做的事情很明显是利用redux的dispatch来触发我们的组件重新加载,这里按图索骥看看这个操作是如何达成的。
首先是更新这块的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 onStateChange ( ) { this .selector.run(this .props) if (!this .selector.shouldComponentUpdate) { this .notifyNestedSubs() } else { this .componentDidUpdate = this .notifyNestedSubsOnComponentDidUpdate this .setState(dummyState) } } notifyNestedSubsOnComponentDidUpdate ( ) { this .componentDidUpdate = undefined this .notifyNestedSubs() }
这个notifyNestedSubsOnComponentDidUpdate的注释怎么理解呢?当我们后面调用this.setState({})后,将会触发组件的更新行为。
假设有一个嵌套发布行为,如果每次发布直接手动计算要不要继续发布(意思是直接发布还是更新组件后再发布),势必每次都要计算这个布尔值。
而通过对componentDidUpdate进行处理,每次发布后只有更新了 才会继续跟进发布。简而言之,这里是一个有条件的遍历行为,只不过利用了componentDidUpdate生命周期。 这里的遍历行为基本是onStateChange函数依托componentDidUpdate&¬ifyNestedSubs进行的。
他的遍历路径大致是这样的:
首先, onStateChange被触发,如果经过计算,需要更新组件,那么设置componentDidUpdate为notifyNestedSubsOnComponentDidUpdate,并使用setState进行更新,更新完毕之后notifyNestedSubsOnComponentDidUpdate被react生命周期componentDidUpdate调用(同时会设置ComponentDid为undefined防止下次被无故调用),它执行的notifyNestedSubs会再次触发onStateChange,如此循环遍历直到不再需要进行组件更新。
到这里这个更新环节如何运行的基本就有脉络了。现在的问题在于订阅后是如何调用到onStateChange的 。
这里订阅如何引起onStateChange?这里在initSubscription这里找找路径。这里主要代码是react-redux/src/utils/Subscription.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export default class Subscription { constructor (store, parentSub, onStateChange ) { this .store = store this .parentSub = parentSub this .onStateChange = onStateChange this .unsubscribe = null this .listeners = nullListeners } addNestedSub (listener ) { this .trySubscribe() return this .listeners.subscribe(listener) } trySubscribe ( ) { if (!this .unsubscribe) { this .unsubscribe = this .parentSub ? this .parentSub.addNestedSub(this .onStateChange) : this .store.subscribe(this .onStateChange) this .listeners = createListenerCollection() } } }
这里核心部分是trySubscribe和addNestedSub里面的this.store.subscribe(this.onStateChange)
和this.listeners.subscribe(listener)
部分。对比之前提到的redux简单用例,可以发现这里就完成了整体的过程的追索。这里的unsubscribe赋值只是从闭包中缓存一个解除订阅的函数指针,这个函数可以方便的直接解除订阅(退订)。
一个成熟项目的结构 前面扯了那么多,最后还是看看一个成熟项目是如何应用react-redux的, 这里代码基本保留主体,删除无关redux代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import { configureStore } from 'app/store' ;const store = configureStore();ReactDOM.render( <Provider store={store}> <Router history={history}> <App /> </Router> </Provider>, document .getElementById('root' ), ); import { Store, createStore, applyMiddleware } from 'redux' ;import { RootState, rootReducer } from 'app/reducers' ;import { rootSaga } from '../sagas' ;export function configureStore (initialState?: RootState ): Store <RootState > { const sagaMiddleware = createSagaMiddleware(); const store = createStore(rootReducer as any, initialState as any, middleware) as Store< RootState > sagaMiddleware.run(rootSaga); return store; } export const rootReducer = combineReducers<RootState>({ routing: routerReducer, global : globalReducer as any, dashBoard: dashBoardReducer as any, ...... }) const initialState: RootState.DashBoardState = from ({ loading: false }); export const dashBoardReducer = handleActions<RootState.DashBoardState, DashBoardModel>( { [dashBoardActions.Type.SAVE]: (state, action ) => { return state.merge(action.payload || {}); }, [dashBoardActions.Type.RESET]: () => { return initialState; }, }, initialState, );
这里项目里面用到了Typescript, 不过忽略它,看看当前redux主流的应用,基本都是基于这个结构了。
关于redux.createReducer在前面已经提到过了,这里就不再说,主要是combineReducers可能要简单提一下,他本质上是将对个对象合并为一个对象,然后传入到createReduce使用。仔细思考一下,其实这样就和前面提到的一模一样了。
Middleware 这里还需要提到的是redux-saga。redux相关操作都是实时的,那么异步只靠它也就无能为力了,但是它定义了中间件接口。通过这个中间件,它可以实现异步的处理。
Redux 的中间件提供的是位于 action
被发起之后,到达 reducer
之前的扩展点,换而言之,原本 view -> action -> reducer -> store
的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store
,在这一环节我们可以做一些 “副作用” 的操作,如 异步请求、打印日志。
一个简单的中间件:
1 2 3 4 5 6 export const logger: Middleware = (store ) => (next ) => (action ) => { if (process.env.NODE_ENV !== 'production' ) { console .log(action); } return next(action); };
这里不对redux-sagas做太多的分析,我们看看一个简单的sagas是怎样写的
1 2 3 4 5 6 7 8 9 10 function * getUserSagas (action: any ): Iterator <any > { const { id } = action.payload; try { const { data } = yield call(AuthApi.getUserDetail, id); yield put(accountEditActions.saveAction({ user : data })); } catch (error) {} } export default function * watchFetchData ( ) { yield takeEvery(accountEditActions.Type.GET_USER, getUserSagas); }
当GET_USER这个action被触发,那么getUserSagas也会被触发。这里非常值得关注的,是这里的generate函数。
这里我们来看看applyMiddleware函数是如何实现的,以便对redux-sagas的实现进行一些分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export default function applyMiddleware (...middlewares ) { return createStore => (...args ) => { const store = createStore(...args) let dispatch = () => { throw new Error ( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI = { getState: store.getState, dispatch: (...args ) => dispatch(...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } export default function compose (...funcs ) { if (funcs.length === 0 ) { return arg => arg } if (funcs.length === 1 ) { return funcs[0 ] } return funcs.reduce((a, b ) => (...args ) => a(b(...args))) } let middleware = applyMiddleware(loggerMiddleware);const store = createStore(rootReducer as any, initialState as any, middleware) as Store<RootState >
除了这些不妨看看redux源码里面createStore里面关于第三个函数的处理:
1 2 3 4 5 export default function createStore (reducer, preloadedState, enhancer ) { ... return enhancer(createStore)(reducer, preloadedState) ... }
综合起来看看这个调用路径做一些思考。思考一下这个体系中他们整体的调用路径:
1 2 3 4 applyMiddleware(loggerMiddleware)-----------+ ↓ createStore(rootReducer, initialState, sagaMiddleware)
看看applyMiddleware的代码结构:
1 2 3 4 5 6 7 8 (...middlewares) => createStore => (...args ) => { const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } }
当middleware被传入到createStore时候,他的结构脱去一层 变成了
1 2 3 4 5 6 7 8 const M = createStore => (...args ) => { const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } }
此时如果设变量M为这个函数那么const store = createStore(rootReducer, initialState, middleware)
实质上是执行了M(createStore)(rootReducer, initialState)
。
那么,此处rootReducer, initialState就是作为…args参数执行进来了。此时呢,middlewares.map(() => {})这块代码又开始有些绕起来了,每个middleware(middlewareAPI)实质上都是这个函数结构被执行
1 2 3 (store) => (next ) => (action ) => { return next(action); }
chain最终返回的结构式这样的:
1 2 3 const chain: Array <(next ) => (action ) => { return next(action); }>
多看看compose函数,假设我们有多个中间件被传入进来,会发生什么呢?
这个变量M里面的dispatch = compose(...chain)(store.dispatch)
最终是这样的一个逻辑:
假设我们最终有个ABC三个中间件,那么dispath最终调用链条是A(B(C(store.dispatch)))。这里一层一层的调用,在执行环节上从里面到外面。对于C来说next是store.dispatch,对于B来说next是C(store.dispatch),对于A来说next是B(C(store.dispatch))。
但是无论如何,最终所有的ABC都获取到了action ,并且这个action并没有什么不同(假设这个action没有被认为修改的话)。
而对于store.dispatch来讲新的dispatch函数(在最终节点 它是store.dispatch(action)这样的调用),其实是在上游对原料做了有点小修改,但是并没有对后面的dispatch操作有任何干涉。
也就是说redux中间件环节上并没有对genertor函数有依赖 ,理解这块对redux-sagas的工作范围有不小帮助。
redux-saga追索 思考 项目中可能写了很多很多saga代码了,大概可以了解到,redux-saga本质上是对generator函数的深入应用。
结合generator特性(即:generator会返回一个迭代器对象,而不是立即执行)、迭代器相关知识、以及redux中间件的理解,可以对saga的原理做出一些猜想(如果对Generator、Iterator了解不够的值得回程票去了解这块)。
这里的猜想是:saga本质上是利用generator函数返回一个根节点迭代器(设其名称为RootSaga),这个迭代器可以通过中间件每次触发事件时候进行完整的遍历,并触发和当前action对应的generator函数(设名称为UserSaga),并在generator利用yield关键词可以在这个函数中像写同步代码一样写相关回调。
在最后也会完整遍历这个UserSaga生成的迭代器。在这个UsserSaga中因为中间件缘故可以获取到state也可以获取dispatch,所以可以对state做读取,也可以做修改,这样就当里面的异步行为完成之后,可以实现异步对redux的state做变更。
探寻 这里找到了最新的提交记录(commit: 10fc193f56f5db05a5ac45140642dbc6a4eed087)
注册 这里来个实际项目里面redux-saga如何注册的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function configureStore (initialState?: RootState ): Store <RootState > { const sagaMiddleware = createSagaMiddleware(); let middleware = applyMiddleware(loggerMiddleware, sagaMiddleware); const store = createStore(rootReducer as any, initialState as any, middleware) as Store< RootState >; sagaMiddleware.run(rootSaga); return store; } const store = configureStore();ReactDOM.render( <Provider store={store}> <Router history={history}> <App /> </Router> </Provider>, document .getElementById('root' ), );
这里看看createSagaMiddleware。这里它实质上redux-saga的default导出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export default function sagaMiddlewareFactory ({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {} ) { let boundRunSaga function sagaMiddleware ({ getState, dispatch } ) { boundRunSaga = runSaga.bind(null , { ...options, context, channel, dispatch, getState, sagaMonitor, }) return next => action => { if (sagaMonitor && sagaMonitor.actionDispatched) { sagaMonitor.actionDispatched(action) } const result = next(action) channel.put(action) return result } } sagaMiddleware.run = (...args ) => { return boundRunSaga(...args) } sagaMiddleware.setContext = props => { assignWithSymbols(context, props) } return sagaMiddleware }
本质上它的返回值和之前提到的
1 2 3 (store) => (next ) => (action ) => { return next(action); }
结构并无二致。{ getState, dispatch }
也无非是对store解构赋值。
后面的sagaMiddleware.run(rootSaga)则实质上调用了boundRunSaga,这个boundRunSaga是对runSaga进行了上下文绑定和传参,但是没有执行。runSaga函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 export function runSaga ( { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError }, saga, ...args ) { const iterator = saga(...args) const effectId = nextSagaId() const env = { channel, dispatch: wrapSagaDispatch(dispatch), getState, sagaMonitor, onError, finalizeRunEffect, } return immediately(() => { const task = proc(env, iterator, context, effectId, getMetaInfo(saga), true , noop) if (sagaMonitor) { sagaMonitor.effectResolved(effectId, task) } return task }) }
其返回值是immediately(cb), 这个immediately正常情况下会将cb原样返回。也就是说,正常情况下返回值是proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, noop)
。而proc返回值,又是newTask函数的返回值,这是一个预先定义好的task对象。
也就是说sagaMiddleware.run
、runSaga
、proc
执行后都返回了一个「task」的对象。
其中我们编写的saga业务代码都在iterator中,这是其中需要重点理解的地方。proc函数是整个redux-saga里面很关键的一个函数,不过这之前,先看看业务代码里面的saga,以及承担分发任务的takeEvery和takeLatest。
分发:takeEvery && takeLatest 这两个分发函数区别在于takeEvery允许多个异步任务同时触发,而takeLatest则一次只能触发一个异步任务,当已有异步任务执行中,那么新的将会被取消。
下面是它们的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 export default function takeEvery (patternOrChannel, worker, ...args ) { const yTake = { done : false , value : take(patternOrChannel) } const yFork = ac => ({ done : false , value : fork(worker, ...args, ac) }) let action, setAction = ac => (action = ac) return fsmIterator( { q1 ( ) { return { nextState : 'q2' , effect : yTake, stateUpdater : setAction } }, q2 ( ) { return { nextState : 'q1' , effect : yFork(action) } }, }, 'q1' , `takeEvery(${safeName(patternOrChannel)} , ${worker.name} )` , ) } export default function takeLatest (patternOrChannel, worker, ...args ) { const yTake = { done : false , value : take(patternOrChannel) } const yFork = ac => ({ done : false , value : fork(worker, ...args, ac) }) const yCancel = task => ({ done : false , value : cancel(task) }) let task, action const setTask = t => (task = t) const setAction = ac => (action = ac) return fsmIterator( { q1 ( ) { return { nextState : 'q2' , effect : yTake, stateUpdater : setAction } }, q2 ( ) { return task ? { nextState : 'q3' , effect : yCancel(task) } : { nextState : 'q1' , effect : yFork(action), stateUpdater : setTask } }, q3 ( ) { return { nextState : 'q1' , effect : yFork(action), stateUpdater : setTask } }, }, 'q1' , `takeLatest(${safeName(patternOrChannel)} , ${worker.name} )` , ) }
这里fsmIterator函数是关键。它生成了一个Iterator,并为这个Iterator自定义了一个next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export default function fsmIterator (fsm, startState, name ) { let stateUpdater, errorState, effect, nextState = startState function next (arg, error ) { if (nextState === qEnd) { return done(arg) } if (error && !errorState) { nextState = qEnd throw error } else { stateUpdater && stateUpdater(arg) const currentState = error ? fsm[errorState](error) : fsm[nextState]() ;({ nextState, effect, stateUpdater, errorState } = currentState) return nextState === qEnd ? done(arg) : effect } } return makeIterator(next, error => next(null , error), name) }
makeIterator这个函数比较简单,这里只简单说下,它主要接收三个三个参数,next函数、抛错函数、name, 根据这个返回一个迭代器对象回来。
那么这里可以返回fsmIterator思考一下它做了什么。仔细观察这个next函数,以及其返回值,可以很容易发现:无论何种原因和状况,只有在nextState === qEnd时候才会才会返回{done: true, value}(done函数返回值),而其他情况下呢,只会不断地执行stateUpdater(q1,q2, q3里面定义的,如果有的话),并返回effect (也是q1,q2, q3里面的)——永不停止,直到有一天,它抛出错误,捕获然后执行done——这时候所有后面所有的异步都不会再执行了,换句话说,你的应用,依赖异步的部分,全挂了。
这个过程如果足够完美,takeEvery地路径是q1->q2->q1->q2….,takeLastet地路径有分支并不固定,但是也是在当前的nextState中,如此循环往复永不歇止。
这种不会停止的遍历循环,正是redux-saga之所以注册一次就可以不断异步响应action的精髓所在。而它一旦停止下来,整个应用也就随之戛然而止——也许大家都有遇到过在一个sagas文件里面没有try…catch捕获错误而导致整个应用所有异步请求全挂的经历。
Side Effect 之所以单独小章节来说,还是因为takeEvery&takeLastest循环往复的遍历过程中会不会的调用到fork、take、cancel这些。
Side Effect包括take|fork|cancel|put|select|call等等这些。
这些都是redux-saga使用时候可以用到的Side Effect,这其中呢,call, select, put尤其常用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 export function put (channel, action ) { if (is.undef(action)) { action = channel channel = undefined } return makeEffect(effectTypes.PUT, { channel, action }) } export function call (fnDescriptor, ...args ) { return makeEffect(effectTypes.CALL, getFnCallDescriptor(fnDescriptor, args)) } export function select (selector = identity, ...args ) { return makeEffect(effectTypes.SELECT, { selector, args }) } const makeEffect = (type, payload ) => ({ [IO]: true , combinator: false , type, payload, }) function getFnCallDescriptor (fnDescriptor, args ) { let context = null let fn if (is.func(fnDescriptor)) { fn = fnDescriptor } else { if (is.array(fnDescriptor)) { ;[context, fn] = fnDescriptor } else { ;({ context, fn } = fnDescriptor) } if (context && is.string(fn) && is.func(context[fn])) { fn = context[fn] } } return { context, fn, args } }
最后返回的结构大致如下:
1 2 3 4 5 6 7 8 9 { "@@redux-saga/IO" : true , "combinator" : false , "type" : "PUT" , "payload" : { "channel" : "xxxx" , "action" : "xxxx" } }
proc函数 proc应该可以拎出来单独说说,前面提到了takeEvery&takelatest返回的迭代器正常情况是一个无限向后遍历的迭代器,但是这个迭代器之前又在另外一个迭代器rootSaga里面,所以我们还需要一个引子来开个头点个火。proc干的就是这事。
他本质上和for…of遍历迭代器没啥区别 ,不过内部还是有很多细节处理。
当我们编写的业务异步代码被执行的时候,执行各种put、call、select各种Side Effect的时候,实质上所有的返回值最后都会返回Side Effect的返回值作为迭代器next的返回值给proc函数。也就是上面提到的那个大致结构。到最后各种情况,风也好雨也罢,最终会执行到effectRunner函数。这个函数来自effectRunnerMap,这是一个Map结构,可以根据上面那个数据结构的type返回一个函数effectRunner,并做出最终处理的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const effectRunnerMap = { [effectTypes.TAKE]: runTakeEffect, [effectTypes.PUT]: runPutEffect, [effectTypes.ALL]: runAllEffect, [effectTypes.RACE]: runRaceEffect, [effectTypes.CALL]: runCallEffect, [effectTypes.CPS]: runCPSEffect, [effectTypes.FORK]: runForkEffect, [effectTypes.JOIN]: runJoinEffect, [effectTypes.CANCEL]: runCancelEffect, [effectTypes.SELECT]: runSelectEffect, [effectTypes.ACTION_CHANNEL]: runChannelEffect, [effectTypes.CANCELLED]: runCancelledEffect, [effectTypes.FLUSH]: runFlushEffect, [effectTypes.GET_CONTEXT]: runGetContextEffect, [effectTypes.SET_CONTEXT]: runSetContextEffect, }
部分effectRunner 这里看最常见的yield call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function runCallEffect (env, { context, fn, args }, cb, { task } ) { try { const result = fn.apply(context, args) if (is.promise(result)) { resolvePromise(result, cb) return } if (is.iterator(result)) { proc(env, result, task.context, currentEffectId, getMetaInfo(fn), false , cb) return } cb(result) } catch (error) { cb(error, true ) } }
这里有fn和args,分别是我们写的generator函数里面传入call的两个参数,可以看到这里call不仅可以接受Promise,它还能接一个一个generator函数这样返回的迭代器可以用proc来遍历。这里generator操作可能会比Promise更加灵活,例如,我们初始化一个3级的级联菜单初始值,如果用promise可能要saga里面手动写三个yield call(PromiseFunction, args), 而generator里面可以直接放三个yield PromiseFunction(args)然后通过proc遍历就可以了。
再看看yield put :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function runPutEffect (env, { channel, action, resolve }, cb ) { asap(() => { let result try { result = (channel ? channel.put : env.dispatch)(action) } catch (error) { cb(error, true ) return } if (resolve && is.promise(result)) { resolvePromise(result, cb) } else { cb(result) } }) }
一般项目里面正常情况put都是直接接到一个action然后执行redux的dispatch(action)。
asap这个函数很常见,主要是用来调度微任务,让当前任务立刻排到当前微任务队列最后面以尽可能快的速度执行。在此处的话,如果我们yield put里面还嵌套一个yield put的话,因为函数执行都是先里层后外层,所以内部嵌套的put会排在前面被先执行。
select 的比较简单就不提及了。
saga的注册 这里需要串联一下前面所有的内容。主要是以下几点
sagaMiddleware.run(rootSaga)
最终调用proc
函数
rootSaga
最终执行返回的迭代最终是takeEvery & takeLatest
返回值
takeEvery & takeLatest
无限遍历,返回值是Side Effect
Side Effect
最终结构为:
1 2 3 4 5 6 7 8 9 { "@@redux-saga/IO" : true , "combinator" : false , "type" : "Fork" , "payload" : { "channel" : "xxxx" , "action" : "xxxx" } }
effectRunner会接收这个参数并做出响应。
鉴于takeEvery里面调用到了take、fork,所以这里最后分析一下下面代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const yTake = { done : false , value : take(patternOrChannel) }const yFork = ac => ({ done : false , value : fork(worker, ...args, ac) })return fsmIterator( { q1 ( ) { return { nextState : 'q2' , effect : yTake, stateUpdater : setAction } }, q2 ( ) { return { nextState : 'q1' , effect : yFork(action) } }, }, 'q1' , `takeEvery(${safeName(patternOrChannel)} , ${worker.name} )` , )
take相关 通常来说take(patternOrChannel)返回值是
1 2 3 4 5 6 { @@redux-saga/IO: true , combinator: false , type: 'TAKE' , payload: { pattern : patternOrChannel }, }
这里看看对应的runTakeEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function runTakeEffect (env, { channel = env.channel, pattern, maybe }, cb ) { const takeCb = input => { if (input instanceof Error ) { cb(input, true ) return } if (isEnd(input) && !maybe) { cb(TERMINATE) return } cb(input) } try { channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null ) } catch (err) { cb(err, true ) return } cb.cancel = takeCb.cancel }
主要的代码还是chanel.take的调用。这里channel常规情况下有一个默认值,来自stdChannel函数的返回值,而这个函数中返回值又来自multicastChannel函数(这两个函数都在packages/core/src/internal/channel.js)。
它返回了一个有put|take|close三个方法的对象。
take所做的事基本就是将cb也就是runTakeEffect函数中的takeCb存到nextTakers中,这个nextTakers在闭包中,可以被其他地方调用put调用到 。这个时候,回头去看看sagaMiddlewareFactory
函数,看看里面的这一块代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function sagaMiddleware ({ getState, dispatch } ) { boundRunSaga = runSaga.bind(null , { ...options, context, channel, dispatch, getState, sagaMonitor, }) return next => action => { if (sagaMonitor && sagaMonitor.actionDispatched) { sagaMonitor.actionDispatched(action) } const result = next(action) channel.put(action) return result } }
关注点放到channel.put(action)
,一切就尽在不言中了——他用于针对特定action注册好函数调用,当中间件收到这个action就立刻调用这个函数。
return这里有个stateUpdater
参数需要注意,我们的迭代(fsmIterator)里面的自定义next函数会判断它,如果有就会被执行,那么在这里,yFork还会通过闭包获取上次take执行时候传进来的action 。
但是这里还有一个地方需要思考,这个回调函数,是如何进一步迭代这个迭代器的呢?要知道,迭代器虽然可以不断遍历,但是仍然需要一只手时不时拨动一下让它不断往下走。这里需要进行参数追溯。其路径是这样:
proc函数中定义了一个next函数作为迭代器自定义的next,其中核心代码是digestEffect(result.value, parentEffectId, next)
,当proc头一次运行时候执行了一次next,然后有一下调用路径:
1 digestEffect->finalRunEffect->runEffect->RunTakeEffect->channel.take
这样next函数通过这一条调用链,被digestEffect函数传参一直被传入到了channel.take里面,然后通过take缓存到了takers数组中,最后被put调用到了的同时,通过遍历takers数组并执行数组中符合action判定的函数——next函数的执行,便能将这个迭代器,每次接到新的action时候往前推进一步。同时,结合刚才提到的stateUpdater
缓存action,我们也能进一步将目光放到fork上了。
fork相关 take看完之后得看看fork, 但是接上文的,我们可以明白take函数呢,至少是在当前这一步最多是执行的将action、next函数缓存下来,对函数执行并没有什么直接的调用,所以这个异步函数的执行,最终还是看fork函数或者更后面的调用。
我们接着take的逻辑往下推,当put执行的时候调用next,然后我们的迭代器被这个函数里面的往前执行了一步,当它执行这个操作的时候,它正停在take对应的q1上,向下一步之后就到了q2, 观察一下源代码发现又走到了runForkEffect(SideEffect)这条线上了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function runForkEffect (env, { context, fn, args, detached }, cb, { task: parent } ) { const taskIterator = createTaskIterator({ context, fn, args }) const meta = getIteratorMetaInfo(taskIterator, fn) immediately(() => { const child = proc(env, taskIterator, parent.context, currentEffectId, meta, detached, noop) if (detached) { cb(child) } else { if (child.isRunning()) { parent.queue.addTask(child) cb(child) } else if (child.isAborted()) { parent.queue.abort(child.error()) } else { cb(child) } } }) }
这里有个createTaskIterator函数,就最简单的应用来讲注1 ,我们这里传进去的fn是我们写的业务代码generator函数,此时返回的就是一个业务generator函数生成的迭代器,同时action、SideEffect相关信息也被传入进去了。因为这个迭代器和takeEvery生成的不同它是有终点的,所以这个生成的child Task到了最后会被完整执行并停止。
这里先返回这个函数来讲,首先是child这个事proc返回一个Task对象,然后执行到后面isRuning() === true,接下来在cb(child),这个cb之前有讲过,实质上是对proc函数内部的next的封装。当我们执行cb(task)时候,这里会触发一个对child迭代器的遍历。这里的脉络是:
cb首先是proc函数传下来的next函数,执行他,如果我们使用takeEvery来注册绑定,那么此时会从q2回到q1。
基于上一条,fork会读取到闭包里面的take Action,这样会触发runTakeEffect,take动作此时才会被执行。
cb也就是next执行时候会触发内部那条result = iterator.next(arg)
,然后这个iterator(业务代码生产的迭代器)就开始往下走,如果它的done值不为true,那么将会由digestEffect来触发runEffect函数,进而触发到达runForkEffect等等,而这些函数里面都会继续进而调用next直到这个迭代器done===true。
思考 takeEvery不是必须。
实现同一个逻辑,我们可以有不同的写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export default function * rootSaga ( ) { takeEvery('INCREMENT_ASYNC' , incrementAsync) } export default function * rootSaga ( ) { while (true ) { yield take('INCREMENT_ASYNC' ) yield incrementAsync() } } export default function * rootSaga ( ) { while (true ) { const action = yield take(globalActions.Type.LOGIN); yield loginSaga(action); } } export default function * rootSaga ( ) { while (true ) { const action = yield take(globalActions.Type.LOGIN); yield fork(() => loginSaga(action)); } }
这里可以试想一下,
为什么takeEvery可以不用while(true){}这种写法?而后面需要?
如果我们后面不写while(true){}这种语法,那么会导致我们的异步仅仅会被触发一次。
当使用了这种写法,按照js语法那肯定是死循环了,但是在generator函数中,配合yield就可以进行中断,可以保证这个迭代器永远不会有done===true那一刻。
为什么redux-saga内部采用这种简单省事的做法?因为redux-saga里面有对异步任务的cancel机制,我们可以需要一个异步任务的执行,如果采用这种机制,则没有取消的余地——takeLatest就是采用了这种机制。
yield fork(() => sagaGeneraotor())和直接yield sagaGeneraotor()调用有什么区别?
这点其实在分析fork函数行为中已经可见一斑了,fork会在next执行过程中采用新建task的方式进行插队并立刻执行,而不用等待后面相关action的完成。
一些有用的包 redux-actions mapDispatchToProps的编写是挺折腾人的。举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export function mapDispatchToProps (dispatch ) { return { changePickerTarget: (index ) => { dispatch(changePickTarget(index)); dispatch(changePickState()); }, changeRoute: (url ) => dispatch(push(url)), togglePick: () => dispatch(changePickState()), toggleDatePick: () => dispatch(changeDatePickState()), fetchPickData: () => dispatch(fetchPickData()), fetchStoreInfo: () => dispatch(fetchStoreInfo()), fetchProjectData: () => dispatch(fetchProjectData()), changeSubmitData: (idx, val ) => dispatch(changeSubmitData(idx, val)), changePickText: (idx, val ) => dispatch(changePickText(idx, val)), submitData: () => dispatch(submitData()), submitCallback: (data ) => dispatch(submitCallback(data)), dispatch, }; }
actions的编写也挺烦:
1 2 3 4 5 6 export function setSubmitStatus (bool ) { return { type: CHANGE_SUBMIT_STATUS, bool, }; }
通过redux-actions我们可以做很大的简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export namespace carInfoListActions { export enum Type { GETLIST = 'carInfoList/GETLIST' , } export const getListAction = createAction<Partial<CarInfoListModel>>(Type.GETLIST); } export type carInfoListActions = Omit<typeof carInfoListActions, 'Type' >;const mapDispatchToProps = (dispatch: Dispatch): any => ({ actions: bindActionCreators(omit(carInfoListActions, 'Type' ), dispatch), }) this .props.actions.getListAction(params);
参考资料 Redux-Saga原始碼解析 - 初始化和take