Connect Redux And React

ReduxAndReact

From the very beginning, we need to stress that Redux has no relation to React. You can write Redux apps with React, Angular, Ember, jQuery, or vanilla JavaScript.

我们经常将redux与react两者放在一起谈论,但实际上Redux与React并没有直接关联,也没有相互依赖的。Redux本质上是一个状态容器,给予开发者维护状态的能力,而这恰恰是React的薄弱环节。

在仅有React的开发中,我们不得不将组件的状态存入state中,子组件需要通过父组件给予的回调函数修改父组件的state,父组件需要把状态通过props传递给子组件。两个组件间需要通信(也可以理解为两个组件需要相互修改状态),就需要将state存储在他们共有的父组件中。随着应用越来越复杂,父组件的state越来越大,传给子组件的props也越来越复杂(即包含数据, 又有回调函数)。

此时Redux出现了,Redux单一数据流的思想和react是不谋而合的。我们可以将整个父组件的state放入Redux的store中进行维护。但我们在本文开头提到过,Redux与React之间是没有关联的,那么如何将Redux与React串联起来呢? 我们使用了react-redux

本质

首先我们先思考一下,我们期望react-redux做一些什么事情?

毫无疑问,一定是构建起Redux与React的桥梁,我们将React中的state提升到Redux的store中,期望在React的view层中能够触发改变Redux store的操作,就像原来的setState一样,我们也期望React组件能够感应到store的change,完成view的重绘。

react-redux也就是这样做的,它非常简洁,仅仅对外暴露了两个API,<Provider store>connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]),API其实非常清晰地给予了顶层组件与子组件的connect方式,如下图:

redux-react

Provider

Provider的作用就在于获取props中的store,将store存储于context中(源码注释1-1),这样所有的后代元素均可以获取到顶层的store,摆脱了父子元素间props的传递。注意,Provider只能包含一个子元素(源码注释1-2),正如上图所示一样,只有一个Root Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Provider extends Component {
getChildContext() {
// 源码注释1-1
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
render() {
// 源码注释1-2
return Children.only(this.props.children)
}
}

Connect

对于一个组件而言, 我们不仅仅需要store的state中所存储的数据,需要获得父组件的props,父组件的props可能是一些数据,也可能是一些配置,也需要获得能够触发store中state改变的action。

那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
const defaultMapStateToProps = state => ({})
const defaultMapDispatchToProps = dispatch => ({ dispatch })
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
// 源码注释2-1
const shouldSubscribe = Boolean(mapStateToProps)
const mapState = mapStateToProps || defaultMapStateToProps
// 源码注释2-2
let mapDispatch
if (typeof mapDispatchToProps === 'function') {
mapDispatch = mapDispatchToProps
} else if (!mapDispatchToProps) {
mapDispatch = defaultMapDispatchToProps
} else {
mapDispatch = wrapActionCreators(mapDispatchToProps)
}
// 源码注释2-3
const finalMergeProps = mergeProps || defaultMergeProps
// 源码注释2-4
const { pure = true, withRef = false } = options
const checkMergedEquals = pure && finalMergeProps !== defaultMergeProps
return function wrapWithConnect(WrappedComponent) {
...
}
}

connect先处理第一个参数mapStateToProps,是否定义该参数,未定义将用defaultMapStateToProps,这将使storeState对组件失去意义,因为默认返回空对象。shouldSubscribe将在之后被用到,可以先放在一边(源码注释2-1)

接下来处理mapDispatchToProps,它会有三种情况(源码注释2-2)。

  • 当参数为一个函数时,mapDispatch即为参数函数,会被传入两个参数,store.dispatch与props。
1
2
3
4
5
6
7
8
9
10
11
12
import * as actionCreators from './actionCreators';
import { bindActionCreators } from 'redux';
function mapStateToProps(state) {
return { todos: state.todos };
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
  • 当参数未定义时,会返回store.dispatch,向组件的props传入,此时往往action都是通过外部引入的,注意:直接调用action是无法改变storeState,引发reduce的,需要用store.dispatch绑定或执行action。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as actionCreators from './actionCreators';
import { bindActionCreators } from 'redux';
function mapStateToProps(state) {
return { todos: state.todos };
}
class TodoApp extends Component {
handleAddTodo() {
const { dispatch } = this.props;
const { addTodo } = actionCreators;
dispatch(addTodo());
}
...
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
  • 当参数为函数对象时,默认使用bindActionCreators将对象中的函数与dispatch进行绑定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as actionCreators from './actionCreators';
function mapStateToProps(state) {
return { todos: state.todos };
}
class TodoApp extends Component {
handleAddTodo() {
this.props.addTodo();
}
...
}
export default connect(mapStateToProps, actionCreators)(TodoApp);

第三个参数mergeProps的默认值将mapStateToProps, mapDispatchToPropsownProps三者合并,交给connect的组件(源码注释2-3)。

第四个参数option包含两个值,purewithRefpure为true的情况下,在connect内部会帮助我们做一些props的浅比较,避免不必要的更新,比如: 比较finalMergePropsdefaultMergeProps的结果。withRef会在之后的源码分析中会再详细讲解。

wrapWithConnect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Connect extends Component {
...
constructor(props, context) {
super(props, context)
// 获取Provider传入的store,顶层props,非顶层context
this.store = props.store || context.store
// this.state与this.store
const storeState = this.store.getState()
this.state = { storeState }
}
...
}

在wrapWithConnect中,首先会从props或context中获取到store存入this.store,将stroe中的state存入this.state,这对之后判断是非更新都会起到非常重要的作用。

1
2
3
4
5
6
7
8
9
10
componentDidMount() {
this.trySubscribe()
}
trySubscribe() {
if (shouldSubscribe && !this.unsubscribe) {
this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
this.handleChange()
}
}

通过trySubscribe,我们会利用this.store.subscribe对每一次action操作做监听,执行handleChange操作。当shouldSubscribe(源码2-1)为false,即mapStateToProps未传入时,就会失去对storeState变化的感知。

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
handleChange() {
if (!this.unsubscribe) {
return
}
const storeState = this.store.getState()
const prevStoreState = this.state.storeState
if (pure && prevStoreState === storeState) {
return
}
if (pure && !this.doStatePropsDependOnOwnProps) {
const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
if (!haveStatePropsChanged) {
return
}
if (haveStatePropsChanged === errorObject) {
this.statePropsPrecalculationError = errorObject.value
}
this.haveStatePropsBeenPrecalculated = true
}
this.hasStoreStateChanged = true
// 同步storeState
this.setState({ storeState })
}

我们主要关注于每一次action后storeState的变化,这里就会使用到之前存在this.state中的storeState,它存储了上一个storeState的值。

mapStateToProps可以接受state(storeState)、 props(ownProps)两个参数,它的返回值stateProps会受到storeState与ownProps双重影响。在这里我们只能知道在mapStateToProps的参数中是否定义了ownProps,却无法得知ownProps是否发生了变化。ownProps发生变化的处理会在其他地方完成,这里专注于storeState对stateProps的影响,可能会让人有种残缺感。

如果没有变化(return),维持this.hasStoreStateChanged默认值false。stateProps将由是否在mapStateToProps定义props参数(this.doStatePropsDependOnOwnProps)与props是否变化所决定。

如果发生变化,且mapStateToProps未定义props参数,也就是stateProps完全受storeState控制。我们就可以直接得出stateProps是否改变,以及stateProps改变后的结果了。

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
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)
return stateProps
}
configureFinalMapState(store, props) {
const mappedState = mapState(store.getState(), props)
const isFactory = typeof mappedState === 'function'
this.finalMapStateToProps = isFactory ? mappedState : mapState
// function.length等于function传入参数的个数
this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1
if (isFactory) {
return this.computeStateProps(store, props)
}
return mappedState
}
updateStatePropsIfNeeded() {
const nextStateProps = this.computeStateProps(this.store, this.props)
if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) {
return false
}
this.stateProps = nextStateProps
return true
}

我们现在需要得出mapStateToProps的返回值stateProps,向computeStateProps传入this.storethis.propsfinalMapStateToProps是传入storeState和props的函数,mapStateToProps所返回的可能是对象,也可能是函数,利用函数来进行一些优化。在react-redux中采用了非常多函数式编程的思想,参数值保持一致,在没有外部依赖的情况下,所得到的结果必然一致。

对于statePropsdispatchPropsownProps三者而言,都有统一的更新入口,分别为updateStatePropsIfNeededupdateDispatchPropsIfNeededupdateMergedPropsIfNeeded。它们实现思想是一致的,将上次的结果存放在this当中,与本次的结果进行比较,以此来避免不必要的更新。

1
2
3
4
5
6
7
8
9
componentWillReceiveProps(nextProps) {
if (!pure || !shallowEqual(nextProps, this.props)) {
this.haveOwnPropsChanged = true
}
}
shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
}

当pure不为true, 父组件传入props发生变化(componentWillReceiveProps判断)以及storeState发生变化(this.store.subscribe(this.handleChange.bind(this)),handleChange判断),都在shouldComponentUpdate前执行。

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
getWrappedInstance() {
return this.refs.wrappedInstance
}
render() {
...
let haveMergedPropsChanged = true
if (
haveStatePropsChanged ||
haveDispatchPropsChanged ||
haveOwnPropsChanged
) {
haveMergedPropsChanged = this.updateMergedPropsIfNeeded()
} else {
haveMergedPropsChanged = false
}
if (!haveMergedPropsChanged && renderedElement) {
return renderedElement
}
if (withRef) {
this.renderedElement = createElement(WrappedComponent, {
...this.mergedProps,
ref: 'wrappedInstance'
})
} else {
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
}
return this.renderedElement
}

在render中主要还是判断是否需要更新,然后将一些标记初始化,待下一次更新时使用。在updateMergedPropsIfNeeded中将stateProps, dispatchPropsownProps合并在一起,作为WrappedComponent的props传入,这样我们就可以在子组件的props中拿到想要的东西了。

withRef会向子组件传递ref值,通过函数getWrappedInstance可进行引用。

ES7 Decorator

connect的常规写法:

1
2
3
4
5
class App extends extends Component {
...
}
export default connect(mapStateToProps, actions)(App)

使用Decorator会使代码更加清晰,connect主要作用就是store与component的连接,作为一个中间层,处理上层给予的storeState,父组件props与actions,整合后作为props给予下层组件。

Decorator对类行为的改变,在代码编译时发生,本质上就是在编译时执行的函数。

1
2
3
4
5
6
7
8
9
10
@connect((state, props) => {
return {
...
}
}, {
...actions
})
class App extends extends Component {
...
}

写了一个简单模拟coonect的示例,Decorator将类作为参数传入,处理完成之后再返回,相当于外层的一次包裹。

参考资料

react docs
react-redux docs
ECMAScript6入门-修饰器