Redux 作为一个状态管理工具,Redux 库本身仅负责状态管理,职责就很纯粹,而与 View 层粘合的部分(比如本文要讲的 React Redux)则单独拆出来。这使做它可以灵活地与多种 View 层实现组合使用,这一点很值得学习。

本文将解析胶水 React Redux 背后的 magic。

使用 React Redux 的一般姿势

Redux 官方文档给出了使用 React Redux 来给 React 集成 Redux 的用法。

根据文档,整体思想是由一个顶级的容器组件来持有和管理 state,子组件作为展示型组件,接受父组件传递的 props 并 render 出来。当然,这些 props 最终来源于容器组件所持有的的全局 state。

首先看官方的示例代码

1
2
3
4
5
6
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

Provider 的角色就是上文中的容器组件。这里将 Provider 作为顶级组件,并将 store 作为 props 传递给它。

再来看对子组件的包装

1
2
3
4
5
6
7
8
import { connect } from 'react-redux'

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

export default VisibleTodoList

对子组件的包装通过 connect 函数来实现,先传递 mapStateToPropsmapDispatchToPropsconnect 得到一个新的函数,再把子组件传进去得到包装后的组件。这个包装出来的组件就会在合适的时机通过某种魔法调用 mapStateToPropsmapDispatchToProps 并将结果作为 props 传递给子组件,这样子组件就可以访问全局 state,或者 dispatch action 了。

其实这里 connect 相关的部分就是高阶组件(Higher Order Component)的角色,它是一个函数,接受组件作为参数,并返回包装后的组件。我们通过 ReactDevTools 可以探查到生成的组件是直接包在我们原来的组件外面的,比如我们有一个叫 Home 的组件,返回的组件树如下。

1
2
3
<Connect(Home)>
<Home/>
</Connect>

那么根据 React Redux 的用法,我们主要的关注点就是两个:Provider 组件与 connect 函数。

Provider 组件

Provider 组件由 createProvider 函数创建。

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
// Provider.js
export function createProvider(storeKey = 'store', subKey) {
const subscriptionKey = subKey || `${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,
}
Provider.displayName = 'Provider'

return Provider
}

export default createProvider()

那么这里的重点就是 getChildContext 方法,它的返回值包含了对 store 的引用。Provider 就是通过 getChildContext 来向组件树提供 context(store)的。

关于 context,它的作用是向下传递数据,与 props 相比,context 不需要一级一级地手动传下去,而下面的任何组件都能够通过 this.context 直接访问 context。

官方文档对 context 有更加详细的解释(Context)。

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
// createConnect with default args builds the 'official' connect behavior. Calling it with
// different options opens up some testing and extensibility scenarios
export function createConnect({
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory
} = {}) {
// 这里就是 connect 函数了
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')

// 返回 connectHOC 调用的结果
return connectHOC(selectorFactory, {
// used in error messages
methodName: 'connect',

// used to compute Connect's displayName from the wrapped component's displayName.
getDisplayName: name => `Connect(${name})`,

// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
shouldHandleStateChanges: Boolean(mapStateToProps),

// passed through to selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,

// any extra options args can override defaults of connect or connectAdvanced
...extraOptions
})
}
}

export default createConnect()

connect 通过 createConnect() 创建出来,connect 的核心实现在 connectHOC(即connectAdvanced)里。

connectAdvanced 返回了函数 function wrapWithConnect(WrappedComponent),也就是说这个 WrappedComponent就是我们自己的组件,而 wrapWithConnect 返回了一个 Connect 组件,它就是最终包装得到的组件。

在 Redux 中,我们要拿到 store 的 state,就应该先去 subscribe 这个 store,然后在 state 更新之后得到通知。我们的组件要想在 state 更新时得到通知,就得靠 Connect 先订阅 store,然后传 props 过来。所以这个 Connect 的实现也是八九不离十了,它应该是先订阅了 store,在 state 更新后再调用 mapStateToPropsmapDispatchToProps,组合之后作为 props 传给子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 {
// 给子组件加了 selector.props 属性,
// 也就是 mapStateToProps, mapDispatchToProps 返回的东西
return createElement(WrappedComponent, this.addExtraProps(selector.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
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
function makeSelectorStateful(sourceSelector, store) {
// wrap the selector in an object that tracks its results between runs.
const selector = {
run: function runComponentSelector(props) {
try {
const nextProps = sourceSelector(store.getState(), props)
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps
selector.error = null
}
} catch (error) {
selector.shouldComponentUpdate = true
selector.error = error
}
}
}

return selector
}

class Connect extends Component {
constructor(props, context) {
super(props, context)

this.state = {}
this.renderCount = 0
// 从 props 或 context 里取出 store
this.store = props[storeKey] || context[storeKey]
this.propsMode = Boolean(props[storeKey])
this.setWrappedInstance = this.setWrappedInstance.bind(this)

// 初始化 this.selector
this.initSelector()
// 订阅 store
this.initSubscription()
}

initSelector() {
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
// 方法定义见上面
// 返回的对象 selector 对象包含一个 run 方法,
// run 方法会调用 selector 函数计算出新的 props,并把 props 作为属性暴露出来,
// 也就是 render 方法中所读取的 selector.props
this.selector = makeSelectorStateful(sourceSelector, this.store)
this.selector.run(this.props)
}

initSubscription() {
if (!shouldHandleStateChanges) return

const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
// Subscription 是用来管理订阅的一个工具类,这里的 onStateChange 函数会接收 state 更新
this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))

this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
}

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
}

// state 有更新
onStateChange() {
// 计算 selector.props
this.selector.run(this.props)

if (!this.selector.shouldComponentUpdate) {
this.notifyNestedSubs()
} else {
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
// 调用 setState 触发 render
this.setState(dummyState)
}
}

notifyNestedSubsOnComponentDidUpdate() {
this.componentDidUpdate = undefined
this.notifyNestedSubs()
}
}

可以看到,Connect 订阅了 store,并在 onStateChange 中令 selector 重新计算 props,之后 setState 触发组件的 render,使子组件的 props 得到更新。

参考资料