react-redux-saga

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
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 {
// 略
}

/* eslint-enable react/no-deprecated */

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' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 3

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)
// 如果不需要更新直接发布 有些store变量没有被组件引用
// 否则设置组件componentDidUpdate生命周期方法 并使用setState空数据进行更新
if (!this.selector.shouldComponentUpdate) {
this.notifyNestedSubs()
} else {
// notifyNestedSubsOnComponentDidUpdate
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
this.setState(dummyState)
}
}

notifyNestedSubsOnComponentDidUpdate() {
// 当`onStateChange`确定需要发布嵌套的subs时,`componentDidUpdate`是有条件触发的
// 一旦这个函数被调用,`componentDidUpdate`不会直接被调用,直到进一步发生状态变化才会被调用。
// 采用这样做法相比使用永久的`componentDidUpdate`每次进行布尔值检查
// 避免了大部分场景下的不必要的方法调用 从而带来一些性能提升。
this.componentDidUpdate = undefined
this.notifyNestedSubs()
}

这个notifyNestedSubsOnComponentDidUpdate的注释怎么理解呢?当我们后面调用this.setState({})后,将会触发组件的更新行为。

假设有一个嵌套发布行为,如果每次发布直接手动计算要不要继续发布(意思是直接发布还是更新组件后再发布),势必每次都要计算这个布尔值。

而通过对componentDidUpdate进行处理,每次发布后只有更新了 才会继续跟进发布。简而言之,这里是一个有条件的遍历行为,只不过利用了componentDidUpdate生命周期。这里的遍历行为基本是onStateChange函数依托componentDidUpdate&&notifyNestedSubs进行的。

他的遍历路径大致是这样的:

首先, 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
// file: App.tsx
import { configureStore } from 'app/store';
const store = configureStore();

ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('root'),
);

// file: app/store/index.ts
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;
}
// file: app/reducers/index.ts
export const rootReducer = combineReducers<RootState>({
routing: routerReducer,
global: globalReducer as any,
dashBoard: dashBoardReducer as any,
......
})

const initialState: RootState.DashBoardState = from({
loading: false
});
// file: dashBoardReducer.ts
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
}
}
}
// compose Function
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
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
export function configureStore(initialState?: RootState): Store<RootState> {
// create the saga middleware
const sagaMiddleware = createSagaMiddleware();
let middleware = applyMiddleware(loggerMiddleware, sagaMiddleware);
const store = createStore(rootReducer as any, initialState as any, middleware) as Store<
RootState
>;

// then run the saga
sagaMiddleware.run(rootSaga);

return store;
}

// main.tsx
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) // hit reducers
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), /* isRoot */ 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.runrunSagaproc执行后都返回了一个「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
// takeEvery
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})`,
)
}
// takeLatest
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
// put
export function put(channel, action) {
if (is.undef(action)) {
action = channel
// `undefined` instead of `null` to make default parameter work
channel = undefined
}
return makeEffect(effectTypes.PUT, { channel, action })
}
// call
export function call(fnDescriptor, ...args) {
return makeEffect(effectTypes.CALL, getFnCallDescriptor(fnDescriptor, args))
}
// select
export function select(selector = identity, ...args) {
return makeEffect(effectTypes.SELECT, { selector, args })
}

// makeEffect
const makeEffect = (type, payload) => ({
[IO]: true,
// this property makes all/race distinguishable in generic manner from other effects
// currently it's not used at runtime at all but it's here to satisfy type systems
combinator: false,
type,
payload,
})
// getFnCallDescriptor
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 }) {
// catch synchronous failures; see #152
try {
const result = fn.apply(context, args)

if (is.promise(result)) {
resolvePromise(result, cb)
return
}

if (is.iterator(result)) {
// resolve iterator
proc(env, result, task.context, currentEffectId, getMetaInfo(fn), /* isRoot */ 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) {
/**
Schedule the put in case another saga is holding a lock.
The put will be executed atomically. ie nested puts will execute after
this put has terminated.
**/
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 effects are non cancellables
}

一般项目里面正常情况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) // hit reducers
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)
}
}
})
// Fork effects are non cancellables
}

这里有个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
// actions
export namespace carInfoListActions {
export enum Type {
GETLIST = 'carInfoList/GETLIST',
}

export const getListAction = createAction<Partial<CarInfoListModel>>(Type.GETLIST);
}

export type carInfoListActions = Omit<typeof carInfoListActions, 'Type'>;
// Page.tsx
const mapDispatchToProps = (dispatch: Dispatch): any => ({
actions: bindActionCreators(omit(carInfoListActions, 'Type'), dispatch),
})
// 调用
this.props.actions.getListAction(params);

参考资料

Redux-Saga原始碼解析 - 初始化和take