React-Redux v5 源码分析

我们知道,Redux是一个独立的状态管理工具,如果想要和React搭配使用,就要借助React-redux。关于它的工作原理网上的教程很多,我大致讲一下,之后主要是分析源码:

首先,Redux完成的事情:

  1. 负责应用的状态管理,保证单向数据流,动作可回溯
  2. 每当应用状态发生变化,触发所有绑定的监听器

好了,以上是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)
  }
}
复制代码

参考资料:

  1. https://github.com/reactjs/react-redux/pull/416
  2. http://blog.nicksite.me/index.php/archives/421.html
  3. https://zhuanlan.zhihu.com/p/32407280
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值