源码阅读之react-redux

210 阅读12分钟

1. 前言

React技术栈中有一环至关重要的就是状态管理,对于状态管理,相信使用redux及react-redux的人不在少数,但是单单去使用它并不能真正的让我们在React应用中得心应手,所以我们需要进入react-redux源码对其一窥究竟,让它更好的为我们服务。

大家回想下,老版本的react-redux是通过在每一个Connect组件中订阅**store**的变化来达到自身的re-render;从大版本6之后,react-redux再次重构了ProviderConnect组件,启用了react@^16.3.x之后新contextAPI。

引用React官网对新的context api的解释:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

简单的说,新版context api能帮助我们将数据传递到层级更深甚至任意层级的组件上,而避免了逐级手动传递的冗余。

context提供一个Provider和一个Consumer,顾名思义Provider是数据提供者,Consumer是数据消费者,一旦Provider提供的数据发生更新,对应的Consumer就会重新渲染。

2. Provider

Provider组件中,store监听state变更


componentDidMount() {
    this._isMounted = true
    this.subscribe() // 组件渲染完成之后监听state变更
}

subscribe() {
    const { store } = this.props

	//一旦store dispatch action,则监听state变更来更新自身的storeState	
    this.unsubscribe = store.subscribe(() => {
      const newStoreState = store.getState()

      if (!this._isMounted) {
        return
      }
		
	   //触发组件更新,更新storeState	
      this.setState(providerState => {
        // If the value is the same, skip the unnecessary state update.
        if (providerState.storeState === newStoreState) {
          return null
        }

        return { storeState: newStoreState }
      })
    })

    ...
}
  
render() {
	const Context = this.props.context || ReactReduxContext 
	// ReactReduxContext即是用React.createContext API新建的context,里面存储着store及 store state
	
	return (
	  //this.state一旦发生更新,Context.Consumer就能接收到更新信息从而自身re-render
	  <Context.Provider value={this.state}>
	    {this.props.children}
	  </Context.Provider>
	)
}

3. connect

大家都知道connect是帮助我们将store state链接到组件的最主要的api。

先带着几个问题:

  • store state是如何注入到子孙组件的props?
  • 为什么dispatch action之后子孙组件能自动更新呢?

3.1 mapStateToProps mapDispatchToProps

首先,大家在用react-redux的时候,最感兴趣的应该是connect函数为什么能以及怎么样将store state注入到组件中去的。

接下来我们逐步分析:

import { connect } from 'react-redux';

相信大家对这行代码肯定很熟悉,connect函数是用来连接我们自己组件,产出最终的高阶组件的。但其实react-redux库中帮我们生成了一个默认的connect函数供大家使用。我们来看一下是如何生成这个connect函数的。

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
    } = {}
  ) {
  	...
  }
}

我们可以清晰的看到connect函数是由createConnect生成的。

  • connectHOC是用于产出高阶组件
  • mapStateToPropsFactories是如何将store state注入子孙组件props的标准化工厂函数数组,默认这里取defaultMapStateToPropsFactories
  • mapDispatchToPropsFactories是如何将dispatch转换成子孙组件的动作方法并注入到props的标准化工厂函数数组,默认这里取defaultMapDispatchToPropsFactories
  • mergePropsFactories是如何将以上两种转换得来的props和自身ownProps做融合merge的标准化工厂函数数组,默认这里取defaultMergePropsFactories
  • selectorFactory是利用以上方法生产出最终组件属性的选择器工厂,这个方法也是react-redux的关键。默认这里取defaultSelectorFactory

我们先看 defaultMapStateToPropsFactories 里面都有什么。

[
	function whenMapStateToPropsIsFunction(mapStateToProps) {
	  return typeof mapStateToProps === 'function'
	    ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
	    : undefined
	},
	function whenMapStateToPropsIsMissing(mapStateToProps) {
	   //此处我们会看到最终会拿到一个空对象
	  return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
	}
]

这里有两种情况:

  1. 当mapStateToProps是函数时,则利用wrapMapToPropsFunc进行转换
  2. 当mapStateToProps缺失时,则利用wrapMapToPropsConstant进行转换
  • wrapMapToPropsConstant做了什么

    function wrapMapToPropsConstant(getConstant) {
      return function initConstantSelector(dispatch, options) {
        const constant = getConstant(dispatch, options)
    
        function constantSelector() {
          return constant
        }
        constantSelector.dependsOnOwnProps = false
        return constantSelector
      }
    }
    

    wrapMapToPropsConstant给我们提供了一个直接返回常量对象的高阶函数,这个高阶函数在selectorFactory中将会生成我们最终的mapStateToProps。至于如何生成的过程我们现在先不说,后面讲到selectorFactory的时候,大家就会明白了。比如在这种情况下,我们最终将得到mapStateToProps如下所示:

    function constantSelector() {
          return constant
    }
    

    以上这个方法并没有形参,实际上mapStateToProps的入参是state与ownProps,因为mapStateToProps缺失的时候,我们最终拿到的就是一个常量对象且为一个空对象,这个空对象是最终组件props的一个快照。

    注意:我们得到了一个高阶函数,记住它的形参(dispatch,options),后续所有获取的高阶函数的形参都是如此,看到这里相信大家也大致明白为什么我们能在组件方法内发起action了,就是利用这个形参dispatch将store的dispatch传入的。我们接着往下看。

  • wrapMapToPropsFunc做了什么

    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.mapToProps = function detectFactoryAndVerify(
          stateOrDispatch,
          ownProps
        ) {
          proxy.mapToProps = mapToProps //此处最后的proxy.mapToProps会被我们传入connenct的mapStateToProps覆盖
          proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
          let props = proxy(stateOrDispatch, ownProps)
    
    		...
    
          return props
        }
    
        return proxy
      }
    }
    

    同样wrapMapToPropsFunc给我们提供了一个高阶函数,这个高阶函数的入参同样是dispatch和options,在selectorFactory中最后会拿到proxy这个函数签名,同时proxy有一个属性mapToProps,在它里面我们能看到传入connect函数的mapStateToProps最终会覆盖proxy.mapToProps,这样我们就能通过mapStateToProps来获取store state了,因为我们从mapStateToProps中返回的就是依靠store state和ownProps转换得到的最终组件props快照。


我们再来看 defaultMapDispatchToPropsFactories 里面都有什么。

[
	function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
	  return typeof mapDispatchToProps === 'function'
	    ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
	    : undefined
	},
	function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
	  return !mapDispatchToProps
	    ? wrapMapToPropsConstant(dispatch => ({ dispatch }))
	    : undefined
	},
	function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
	  return mapDispatchToProps && typeof mapDispatchToProps === 'object'
	    ? wrapMapToPropsConstant(dispatch =>
	        bindActionCreators(mapDispatchToProps, dispatch)
	      )
	    : undefined
	}
]

我们会发现和mapStateToProps的情况非常类似,这也就是react-redux为什么会有这么一步标准化工厂生产的过程的原因

但这里有三种情况:

  1. 当mapDispatchToProps是函数时,则利用wrapMapToPropsFunc进行转换
  2. 当mapDispatchToProps缺失时,则利用wrapMapToPropsConstant进行转换,但这里我们拿到的不是空对象{}而是一个包含属性dispatch的对象{dispatch}。
  3. 当mapDispatchToProps是一个对象时,则利用wrapMapToPropsConstant进行转换,只不过这里的常量对象是一个经过包装得到的actionCreators。

现在我们知道标准化的mapStateToProps,mapDispatchToProps是如何产生的了,让我们回过头看下connect函数的其他部分。

function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) => {
    throw new Error(
      `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${
        options.wrappedComponentName
      }.`
    )
  }
}

match函数的形参factories就是我们上面讲过的那几个factories。

const initMapStateToProps = match(
  mapStateToProps,
  mapStateToPropsFactories,
  'mapStateToProps'
)
const initMapDispatchToProps = match(
  mapDispatchToProps,
  mapDispatchToPropsFactories,
  'mapDispatchToProps'
)
const initMergeProps = match(
	mergeProps, 
	mergePropsFactories, 
	'mergeProps'
)

从以上代码中,我们就能知道前面说到的用于产出属性映射选择器的高阶函数,是在match函数里得到的,从函数签名也可以对照起来。

3.2 selectorFactory

接下来我来给大家讲解一下selectorFactory的作用。首先我们要了解的是selectorFactory在哪里被调用。正如它的名字一样,选择器工厂,选择器工厂生成的选择器是用来选择什么的?那当然是我们的store state。

在connect函数里,我们能看到这样一段源码:

return connectHOC(selectorFactory, {
   
     ...
     
      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

	 ...
	 
    })

我们能看到selectorFactory以及initMapStateToProps、initMapDispatchToProps、initMergeProps都被传入了connectHOC,这里默认的connectHOC是取connectAdvanced。

题外话:其实我们会发现react-redux作者做了很多的抽象与解耦,从createConnect到connectHOC的可定制化,给了我们更多可以个性化定制高阶组件的空间,不禁要赞一下react-redux作者的编程思想。

那么connectHOC里面做了什么呢?我相信大家的第一反应肯定是用来生产出我们的高阶组件的,对,这肯定是它最关键的作用。

从上面的分析我们知道在connect函数里用的selectorFactory是defaultSelectorFactory,那么defaultSelectorFactory是怎么样的,它里面又做了什么呢?

function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

   ...
  
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}

在这个defaultSelectorFactory中我们会发现initMapStateToProps、initMapDispatchToProps及initMergeProps逐一被调用(大家应该还记得它们的形参吧?),生成了我们最终用来映射store state和dispatch的mapStateToProps、mapDispatchToProps、mergeProps。

同时以上代码中的options都是从connect函数的最后一个参数里透传进来的,我们可以看到默认的pure属性是true,因此这里拿到的selectorFactory是pureFinalPropsSelectorFactory。

我们再看pureFinalPropsSelectorFactory又做了什么?

function pureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
	
   let hasRunAtLeastOnce = false
   let state
   let ownProps
   let stateProps
   let dispatchProps
   let mergedProps
   
	...
	
	return function pureFinalPropsSelector(nextState, nextOwnProps) {
	    return hasRunAtLeastOnce
	      ? handleSubsequentCalls(nextState, nextOwnProps)
	      : handleFirstCall(nextState, nextOwnProps)
    }
}

我们看到这个pureFinalPropsSelectorFactory返回了我们最终要用的selector。在handleFirstCall、handleSubsequentCalls中我们会发现由defaultSelectorFactory生成的mapStateToProps、mapDispatchToProps、mergeProps这三个函数开始真正的执行。

function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    stateProps = mapStateToProps(state, ownProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    return mergedProps
}

function handleSubsequentCalls(nextState, nextOwnProps) {
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps

    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
}

同时在这个方法内,作者做了一个优化,通过一个闭包缓存了上一次选择器的state、ownProps、stateProps、dispatchProps、mergedProps。这样就能通过对比前后两次结果是否发生改变来决定是否要进行一次选择映射。

我们来重点回顾一下

  • mapStateToProps、mapDispatchToProps、mergeProps先是由我们通过形参传入connect函数,在经过标准化工厂封装产出对应的高阶函数

  • 再传入defaultSelectorFactory,生成最终的标准化的mapStateToProps,mapDispatchToProps、mergeProps

3.3 connectAdvanced

现在轮到connectAdvanced闪亮登场了,在connect函数里我们最终返回的就是由connectAdvanced函数生成的高阶组件。我们现在来看下它里面做了什么:

首先我们来看下最最重要的选择器是如何产生的:

function makeDerivedPropsSelector() {
  let lastProps
  let lastState
  let lastDerivedProps
  let lastStore
  let lastSelectorFactoryOptions
  let sourceSelector

  return function selectDerivedProps(
    state,
    props,
    store,
    selectorFactoryOptions
  ) {
	//通过上一次的state与新的state的对比来判断是不是需要进行一次选择器选择映射
    if (pure && lastProps === props && lastState === state) {
      return lastDerivedProps
    }

    if (
      store !== lastStore ||
      lastSelectorFactoryOptions !== selectorFactoryOptions
    ) {
      lastStore = store
      lastSelectorFactoryOptions = selectorFactoryOptions
	  //调用选择器工厂,并传入dispatch
      sourceSelector = selectorFactory(
        store.dispatch,
        selectorFactoryOptions
      )
    }
	//对state、props做缓存
    lastProps = props
    lastState = state
	//通过selector拿到准备注入WrapperedComponent中的最新的props
    const nextProps = sourceSelector(state, props)

    lastDerivedProps = nextProps
    return lastDerivedProps
  }
}

我们知道这里的selectorFactory是defaultSelectorFactory,是从connect函数中传入到connectAdvanced中的,selectorFactoryOptions都是从connect函数中透传下来的额外配置属性,里面包括了initMapStateToProps, initMapDispatchToProps, initMergePropsmakeDerivedPropsSelector的调用时机后面会讲到,他调用后生成了我们最终的属性选择器,然后我们会发现它的形参里面有很关键state、props及store。在它的函数体里面,dispatch被传入到selectorFactory中,这个dispatch正是store.dispatch。那我们生成的sourceSelector的函数是什么样的呢?

经过上面的分析,它应该是这样的:

function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
}

里面会调用最终的标准化的mapStateToProps、mapDispatchToProps、mergeProps,这样也串联了我们上面分析的。

store及store state又是如何传入的呢?这里就是高阶组件的具体实现了。在高阶组件内部有一个Connect组件,它承载了store注入的工作及与祖父组件的联通。

在Connect这个React组件的constructor中我们看到

this.selectDerivedProps = makeDerivedPropsSelector()
this.selectChildElement = makeChildElementSelector()
this.indirectRenderWrappedComponent =this.indirectRenderWrappedComponent.bind(this)

上面我们提到的makeDerivedPropsSelector,它就是在Connect这个React组件的constructor中被调用,生成的selectDerivedProps会在组件渲染时触发:


indirectRenderWrappedComponent(value) {
    //calling renderWrappedComponent on prototype from indirectRenderWrappedComponent bound to `this`
    return this.renderWrappedComponent(value)
}

renderWrappedComponent(value) {
    
    ...
    
    const { storeState, store } = value

    let wrapperProps = this.props
    let forwardedRef

    ...

    let derivedProps = this.selectDerivedProps(
      storeState,
      wrapperProps,
      store,
      selectorFactoryOptions
    )

    return this.selectChildElement(
      WrappedComponent,
      derivedProps,
      forwardedRef
    )
}

我们再结合Connect组件的render方法看一下:

render() {
	const ContextToUse =
	  this.props.context &&
	  this.props.context.Consumer &&
	  isContextConsumer(<this.props.context.Consumer />)
	    ? this.props.context
	    : Context
	
	return (
	  <ContextToUse.Consumer>
	    {this.indirectRenderWrappedComponent}
	  </ContextToUse.Consumer>
	)
}

本文最上面已经介绍到React新的context

在Provider中我们将store及store state传入了context里,至于为什么在Provider中将store state单独也放入context中,是因为在makeDerivedPropsSelector通过闭包将上一次的state值缓存在lastState中,用于比较前后state是否发生更新的优化处理。

在Consumer中通过给children传递一个函数我们能拿到这些传入的值。也就是renderWrappedComponent的形参value中,我们能拿到store及store state,这样通过selectDerivedProps函数将拿到到的state及ownProps下放到pureFinalPropsSelector中,就能通过mapStateToProps、mapDispatchToProps转换得到derivedProps并注入到我们自己声明的WrappedComponent组件了,最后通过在WrappedComponent中dispatch action来更新store state并触发在Provider中监听的state更新事件来达到组件树的状态联通。

connect函数将store state及dispatch都转换成了props传入到我们的WrappedComponent组件上来,同时在Connect组件上我们会发现作者做了一个小优化,就是根据pure属性来决定是继承自Component还是PureComponent,由于Connect组件在实际代码中,只会接收到上层组件传递的属性,这样就能减少额外的re-render。

4. 总结

react-redux几经改版,大版本6利用了新的context api,但纵观全部源码,不难发现其本质思想没有改变。

  • 将我们传递给connect函数的mapStateToProps、mapDispatchToProps经过标准化工厂进行封装
  • 传入selectorFactory获取最终的selector函数,并在其中调用标准化之后的mapStateToProps、mapDispatchToProps来产出derivedProps,灌入WrappedComponent
  • 利用store的subscribe监听dispatch action,最后更新store state达到更新组件树的目的。

最后经过理论分析,个人认为这一版react-redux@6.x.x比以往的版本在代码架构上更简洁明了,同时性能也提升了不少,这从作者的各种优化思想的点上不难看出。同时再得益于React@16.x.x的fiber机制及新的reconcilation机制,更加相得益彰。

通过以上分析,希望对大家理解react-redux能有所帮助,如果有什么分析不对的地方,希望大家果断指出来,一起研究,共同进步!