我们知道,Redux是一个独立的状态管理工具,如果想要和React搭配使用,就要借助React-redux。关于它的工作原理网上的教程很多,我大致讲一下,之后主要是分析源码:
首先,Redux完成的事情:
- 负责应用的状态管理,保证单向数据流,动作可回溯
- 每当应用状态发生变化,触发所有绑定的监听器
好了,以上是Redux的工作。那么,结合React,我们会如何使用呢?
第一个问题,组件如何读取store中的值
如果我们直接将store作为最外层容器的props传入,那么所有子组件都需要去一级一级传递这个对象,显然太过麻烦。ReactRedux这里使用了在最外层声明context的方式,供子组件自由读取。
第二个问题,子组件如何监听store中state的变化
我们希望当store中某个state发生了变化,只重绘用到了这个state的组件,而不是整个应用。这里ReactRedux是通过使用高阶组件的方式,将你需要连接store的组件包裹起来,通过传入一系列过滤函数:mapStateToProps, mapDispatchToProps, mergeProps等,以及options中equals判断函数,最终最小范围地监听state变化。监听到变化后,便会触发connect中的onStateChange,引起组件的重绘
接下来我们来看源码
文件目录结构:
connect.js
// 这里通过函数调用的方式,将默认的工厂函数传入,是考虑到了扩展性和便于测试
export function createConnect({
connectHOC = connectAdvanced, // 这个是最核心的,connect高阶组件
mapStateToPropsFactories = defaultMapStateToPropsFactories, // mapStateToProps工厂函数
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, // mapDiaptchToProps工厂函数
mergePropsFactories = defaultMergePropsFactories, // mergePropsFactories工厂函数
selectorFactory = defaultSelectorFactory // selector工厂函数,这里解释一下selector的主要作用就是使用上面三个函数,筛选出最后的mergedProps
} = {}) {
// 这里就是我们实际使用到的connect第一次调用函数
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
/**
默认浅比较,这也就是为什么我们在reducer中通常会这么写:
(state = { count: 1 }, action) => {
const nState = cloneDeep(state)
switch (action.type) {
case 'ADD':
nState.count += action.value
}
return nState
}
如果这里没有返回一个新的Object,mapStateToProps方法的返回结果 === prevState,则组件不会更新
**/
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
) {
// 这里可能很多人会奇怪,match操作做了些什么事呢,后面会说到
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})`,
// 如果mapStateToProps是个falsy的值,则这个组件就不会去监听store上state的变化
shouldHandleStateChanges: Boolean(mapStateToProps),
// 这些值都会透传给selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
...extraOptions
})
}
}
复制代码
wrapMapToProps.js
/**
这里就是match操作主要做的事情了,这里使用了一个proxy,完成了以下对mapToProps方法的初始化工作:
1. 计算出mapToProps这个函数是否依赖ownProps。
为什么要去计算是否依赖ownProps呢,每次调用mapToProps都将其传入不好吗?
因为如果mapToProps不依赖组件的ownProps,可以节省计算。举个栗子:
某次dispacth操作修改了store上的state,但是state的变化没有影响到我的组件,只是组件所接受到的props发生了变化。
那么,如果我的mapToProps不依赖ownProps,我就不需要重新计算mapStateToProps和mapDispatchToProps了
2. 递归调用mapToProps方法,将它最终返回的那个function,作为真正使用的函数。这个操作只执行一次
3. 在非生产环境,会判断第一次调用mapToProps方法得到结果是不是一个纯对象,不是的话会报错
**/
export function wrapMapToPropsFunc(mapToProps, methodName) {
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
// 这里的操作很关键,改变了proxy.mapToProps的指向,使得后面的代码只会被执行一次
proxy.mapToProps = mapToProps
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
let props = proxy(stateOrDispatch, ownProps)
if (typeof props === 'function') {
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
if (process.env.NODE_ENV !== 'production')
verifyPlainObject(props, displayName, methodName)
return props
}
return proxy
}
}
复制代码
// 这是最关键的文件,我们重点看一下
connectAdvanced.js
// 经过前面的分析,我们知道Selector的功能是使用用户自定义的mapToProps方法,筛选出组件监听state的范围,这里就是Selector真正调用的地方了
function makeSelectorStateful(sourceSelector, store) {
const selector = {
run: function runComponentSelector(props) {
try {
// 可以看到,Selector在这类被执行,传入了新的state和ownProps
const nextProps = sourceSelector(store.getState(), props)
// 如果经过Selector筛选后,返回的nextProps和prevProps的结果浅相同,那么不执行组件更新
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps // 这里会缓存本次的计算结果,作为下一次计算的prevProps
selector.error = null
}
} catch (error) {
selector.shouldComponentUpdate = true
selector.error = error
}
}
}
return selector
}
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++
return function wrapWithConnect(WrappedComponent) {
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]) // 判断store是否来自props
this.setWrappedInstance = this.setWrappedInstance.bind(this)
this.initSelector() // 初始化Selector
this.initSubscription() // 初始化监听
}
getChildContext() {
// 判断如果是propsMode,则不加入subscription订阅链。一般也很少有人会将store作为props参数传递吧
const subscription = this.propsMode ? null : this.subscription
// 这里的动作是将当前组件实例上挂载的subscription对象,添加到context中,以供子组件读取
// 这样就保证了所有子组件可以访问到其父组件的subscrition对象
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
componentDidMount() {
if (!shouldHandleStateChanges) return
// 在componentDidMount方法里做trySubscribe操作,而不是在componetWillMount,是为了照顾SSR的情况
// 因为在SSR时,componetWillUnmount不会被执行,也就是unsubscribe不会被执行,就会造成内存泄露
// 有一种情况下,我们可能在componentWillMount中就dispatch了一个action,修改了state,那么可能就监听不到了
// 所以下面会再次执行一遍selector.run方法,保证渲染的准确性
this.subscription.trySubscribe()
this.selector.run(this.props)
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
componentWillUnmount() {
if (this.subscription) this.subscription.tryUnsubscribe()
this.subscription = null
this.notifyNestedSubs = noop
this.store = null
this.selector.run = noop
this.selector.shouldComponentUpdate = false
}
initSelector() {
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) // 将dispatch和那些mapToProps方法传入
this.selector = makeSelectorStateful(sourceSelector, this.store) // 暴露一个run方法供调用,缓存每次的计算结果
this.selector.run(this.props) // 第一次调用,进行一些初始化操作,此时不会执行各个equals方法
}
/**
这里需要重点说一下React-Redux V5的事件订阅模型:
如果我们把所有的组件的onStateChange事件,订阅到store.subscribe上,子组件可能受到父组件渲染影响,而导致多次渲染。
React-Redux从5.0版本开始,connect被重写,增加层级(嵌套)观察者,保证事件通知与组件更新的顺序
更为细致的分析,可以看这里: http://blog.nicksite.me/index.php/archives/421.html
**/
initSubscription() {
// 记得我们在调用connectHOC方法时,传入的这个参数吗,就是判断了下mapStateToProps是不是一个falsy的值
if (!shouldHandleStateChanges) return
// 获取父组件的subscription对象
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)
}
// 每次state发生时,被触发的函数
onStateChange() {
this.selector.run(this.props)
if (!this.selector.shouldComponentUpdate) {
// 如果本次组件自身不更新,通知子组件更新
this.notifyNestedSubs()
} else {
// 如果本次组件需要更新,则在更新完成后(didUpdate),通知子组件更新
// 如果本次re-render,同时触发了子组件的re-render,那么即便通知更新,子组件也不会重绘了
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
this.setState(dummyState)
}
}
notifyNestedSubsOnComponentDidUpdate() {
this.componentDidUpdate = undefined
this.notifyNestedSubs()
}
isSubscribed() {
return Boolean(this.subscription) && this.subscription.isSubscribed()
}
render() {
const selector = this.selector
selector.shouldComponentUpdate = false
if (selector.error) {
throw selector.error
} else {
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
}
// 这个方法是将wrappedComponent上的非React提供的静态方法,添加到Connect上
return hoistStatics(Connect, WrappedComponent)
}
}
复制代码
参考资料:
- https://github.com/reactjs/react-redux/pull/416
- http://blog.nicksite.me/index.php/archives/421.html
- https://zhuanlan.zhihu.com/p/32407280