前言

之前已经分析过,redux的源码。现在再来看一下react-redux的源码吧。

还是和之前一样,我习惯带着问题去看源码,关于react-redux我有如下几个问题:

  1. Provider的作用
  2. connect的作用原理
  3. react-redux如何促发组件更新

源码分析

react-redux的源码结构如下:

│  index.js
│
├─components
│      connectAdvanced.js   
│      Provider.js    
│
├─connect
│      connect.js 
│      mapDispatchToProps.js
│      mapStateToProps.js
│      mergeProps.js 
│      selectorFactory.js
│      verifySubselectors.js
│      wrapMapToProps.js
│
└─utils
        PropTypes.js
        shallowEqual.js
        Subscription.js 
        verifyPlainObject.js
        warning.js
        wrapActionCreators.js

老样先看index.js

import Provider, { createProvider } from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import connect from './connect/connect'

export { Provider, createProvider, connectAdvanced, connect }

很明显,这是一个暴露react-redux方法成员的文件,下面我们就根据这些成员方法一个一个进行分析。

components/Provider

import { Component, Children } from 'react'
import PropTypes from 'prop-types'
import { storeShape, subscriptionShape } from '../utils/PropTypes'
import warning from '../utils/warning'

let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
	...
}

// 返回一个createProvider的方法,重新构造Provider
export function createProvider(storeKey = 'store', subKey) {
    const subscriptionKey = subKey || `${storeKey}Subscription`
	// 类名Provider的react组件类
    class Provider extends Component {
      	// 返回整个store
        getChildContext() {
          return { [storeKey]: this[storeKey], [subscriptionKey]: null }
        }

        constructor(props, context) {
          super(props, context)
          this[storeKey] = props.store; // 提取传进来的store
        }
		// 只能返回单一的children,渲染
        render() {
          return Children.only(this.props.children)
        }
    }

    if (process.env.NODE_ENV !== 'production') {
      Provider.prototype.componentWillReceiveProps = function (nextProps) {
        if (this[storeKey] !== nextProps.store) {
          warnAboutReceivingStore()
        }
      }
    }

    Provider.propTypes = {
        store: storeShape.isRequired,
        children: PropTypes.element.isRequired,
    }
    Provider.childContextTypes = {
        [storeKey]: storeShape.isRequired,
        [subscriptionKey]: subscriptionShape,
    }

    return Provider
}
// 返回Provider组件类
export default createProvider()

从上面的源码和注释我们可以看出来,Provider.js主要提供了Provider组件类和一个重新构造Provider的函数createProvider(),获取传进来的store,赋值到this[store]。到这第一个问题我们也就大致明白了。接下来我们先反过来看connect,再看connectAdvanced

connect

在了解connect源码之前,我们先看看它怎么用:

    import React from "react"
    import ReactDOM from "react-dom"
    import { bindActionCreators } from "redux"
    import {connect} from "react-redux"
    
    class xxxComponent extends React.Component{
        constructor(props){
            super(props)
        }
        componentDidMount(){
            this.props.aActions.xxx1();
        }
        render (
            <div>
                {this.props.$$aProps}
            </div>
        )
    }
    
    export default connect(
        state => ({
            $$aProps: state.$$aProps,
            $$bProps: state.$$bProps,
            // ...
        }),
        dispatch => ({
            aActions: bindActionCreators(AActions,dispatch),
            bActions: bindActionCreators(BActions,dispatch),
            // ...
        })
    )(xxxComponent)

看上面的例子我们就能知道connnect作用有如下几点:

  1. 使用了react-reduxconnect后,我们导出的对象不再是原先定义的xxx Component,而是通过connect包裹后的新React.Component对象。
  2. connect中包装过的stateaction在组件中可以从this.props提取出来,可见作为参数传入的stateaction,在connect内部进行合并,通过props的方式传递给包裹后的ReactComponent

再来看一下connect的源码:

// ...
export function createConnect({
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
 // 传入connect参数, 返回connectAdvanced的执行结果
  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
    })
  }
}

// 返回默认的connect函数
export default createConnect()

所以connect主要做的是执行了connectAdvanced,获取其返回结果。我们接着再看回connectAdvanced的源码,去了解connect怎么把数据绑到this.props上,并返回一个新的组件。

component/connectAdvanced

源码有点长,我截取其中关键部分进行分析:

  // ...
  export default function connectAdvanced(){
    //...
    return function wrapWithConnect(WrappedComponent) {
        //...
      	const contextTypes = {
          [storeKey]: storeShape,
          [subscriptionKey]: subscriptionShape,
        } // 和最下方的代码结合,限定传进来的数据的数据类型
        //...
        class Connect extends Component {
           // 提取全局的store变量
            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])
                this.setWrappedInstance = this.setWrappedInstance.bind(this)

                invariant(this.store,
                  `Could not find "${storeKey}" in either the context or props of ` +
                  `"${displayName}". Either wrap the root component in a <Provider>, ` +
                  `or explicitly pass "${storeKey}" as a prop to "${displayName}".`
                )

                this.initSelector()
                this.initSubscription()
            }
          	// ...
            // 设置上下文context内容
            getChildContext() {
              const subscription = this.propsMode ? null : this.subscription
              return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
            }

            // 周期方法及操作方法
            // ...
          	initSelector() {
              const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
              this.selector = makeSelectorStateful(sourceSelector, this.store)
              this.selector.run(this.props)
            }
            //...
            render() {
              const selector = this.selector
              selector.shouldComponentUpdate = false

              if (selector.error) {
                throw selector.error
              } else {
                // createElement(reactClass, [object props], [children])
                // 三个参数,第一个:标签名,第二个:props,第三个:children
                // 返回一个reactElement
                return createElement(WrappedComponent, this.addExtraProps(selector.props))
              }
            }
        }
        //hoistStatics作用(官方):Copies non-react specific statics from a child component to a parent
        //component. 
        //Similar to Object.assign, but with React static keywords blacklisted from being overridden.
      	// 作用是把WrappedComponent中所有的静态方法复制到Connect中
        return hoistStatics(Connect, WrappedComponent);
    }
    Connect.childContextTypes = childContextTypes // 为子级定义上下文类型,与getChildContext()结合,传
    											  // 递上下文给子组件
    Connect.contextTypes = contextTypes // 设置当前上下文类型
    Connect.propTypes = contextTypes // 设置传进来的props的数值类型
    //...
  }

从这段代码我们可以看出connectAdvanced返回wrapWithConnect()函数,该函数以WrappedComponent(connect的组件)为参数,先创建一个connectReactComponent对象,最终返回一个ReactComponent对象,hoistStatics()的作用就是把WrappedComponent这个组件内的所有静态方法,加入到新的组件connect当中。回头再看connect方法,返回值是执行connectAdvanced()后的结果,即connect方法最终返回的是一个经过包装的ReactComponent对象。基本的运行过程明白了,我们再来看一下他是如何实现把stateaction封装到this.props当中。实现这个效果的关键代码在selectFactory.js

selectFactory.js

官方英文翻译,selectorFactory函数返回一个selector函数,根据store state, 展示型组件props,和dispatch计算得到新props,最后注入容器组件。再来看一下部分源码:

import verifySubselectors from './verifySubselectors'
// impureFinalPropsSelectorFactory做法比较暴力,每次都会生成一个新的属性对象
export function impureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch
) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    ) //把上面的三个对象和并成一个对象,然后得到Connnect组件的属性值,然后进行注入,看一下mergeProps文件
  }
}

export function pureFinalPropsSelectorFactory(
   // ...
}
// 
export default function finalPropsSelectorFactory(dispatch, {
  initMapStateToProps,
  initMapDispatchToProps,
  initMergeProps,
  ...options
}) {
  // ...
  // 判断用哪种执行器
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory
  // 最终返回一个selectorFactory执行器
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}

我们从上面注释可以看出selectorFactory有两种处理方式,一种是impureFinalPropsSelectorFactory,而 另一种pureFinalPropsSelectorFactory, 它会缓存上一次生成的 属性值,然后每当要创建新的selector的时候,它会新进行判断 state是否更新,ownProps是否进行更新,如果都不进行更新的话,直接返回上一次缓存的值,代码有点多,这里不细讲了。到这里,第二个问题基本弄清楚了,接下来是第三个问题,我们知道,每当setState一次,react组件就会重新渲染,但是和redux关联后,你直接setStateredux的数据并不能直接触发react组件的更新,只是更改了redux的数据而已,接下来就让我们看看react-redux是怎么做到让组件重新渲染的.

关于react-redux如何使组件重新渲染

redux实际也是一个事件的订阅机制,它和react联系主要在Connect组件,它会订阅Connect组件里面的onStateChange函数,当redux进行dispatch的时候,就会触发Connect组件的onStateChange函数。订阅的代码在connect组件里:

 initSubscription() {
        if (!shouldHandleStateChanges) return
        const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
        // 这里可以看一下Subscription.js源文件,主要就是调用了redux,store里面的subscribe方法
      // 注意这里传的是父的订阅器
        this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))
        this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
      }

再来看一下onStateChange的代码:

onStateChange() {
        this.selector.run(this.props) // 计算当前的props,判断是否与前一次为同一个对象,此时props已更新
        if (!this.selector.shouldComponentUpdate) { 
          // 如果是同一个对象,则不需要更新直接发布,同时提醒子元素
          this.notifyNestedSubs() // 促发所有listener函数,即子元素的onStateChange函数
        } else {
          // 如果不是同一个对象,清除组件componentDidUpdate生命周期方法
          this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
          this.setState(dummyState) // dummyState = {},利用设置空对象触发更新,把props换到最新的
        }
      }

this.notifyNestedSubs()做了什么呢?其实每个Connect组件里面都有一个subscription对象,它也是一个订阅模型,每个父的Connect订阅的是 子Connect组件的onStateChange函数,而父的Connect的onStateChange函数,被谁订阅呢?当然是store(redux), 即流程为dispathc(action)--- 触发store的订阅即父的onStateChange --- 父的onStateChange触发即触发子Connect的onStateChange,这样就能层层更新了.详细的订阅细节可以看一下,Subscription.js的文件,和redux的类似,因为比较简单,这里就不细说了。要注意的是他是为父订阅器添加子的onStateChange函数即可。

最后当组件unmount后,应当取消订阅,提高性能。源代码如下:

componentWillUnmount() {
        //容器组件卸载后,取消当前的订阅
        if (this.subscription) this.subscription.tryUnsubscribe() //取消下面子Connect的更新
        this.subscription = null
        this.notifyNestedSubs = noop
        this.store = null
        this.selector.run = noop
        //设置为不可以更新---!!!这个也是为什么,redux管理下的Connect容器组件在调用异步时,
        //异步里面有setState,然后立即退出,这个时候异步还没完成,但是组件已经unmount了,
        //而组件并没有发生 在unmount组件上面使用setState错误的原因 !!!
        this.selector.shouldComponentUpdate = false
      }

参考

  1. https://segmentfault.com/a/1190000007589792
  2. https://www.jianshu.com/p/6d0581619ab4
  3. http://blog.codingplayboy.com/2017/09/25/react-redux/