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
函数来实现,先传递 mapStateToProps
,mapDispatchToProps
给 connect
得到一个新的函数,再把子组件传进去得到包装后的组件。这个包装出来的组件就会在合适的时机通过某种魔法调用 mapStateToProps
,mapDispatchToProps
并将结果作为 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
| 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
|
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,
...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 更新后再调用 mapStateToProps
和 mapDispatchToProps
,组合之后作为 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 { 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) { 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 this.store = props[storeKey] || context[storeKey] this.propsMode = Boolean(props[storeKey]) this.setWrappedInstance = this.setWrappedInstance.bind(this)
this.initSelector() this.initSubscription() }
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) }
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 }
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() } }
|
可以看到,Connect
订阅了 store,并在 onStateChange
中令 selector 重新计算 props,之后 setState 触发组件的 render,使子组件的 props 得到更新。
参考资料