React Redux: 从文档看源码 - Components篇

注:这篇文章只是解说React Redux这一层,并不包括Redux部份。Redux有设计去进修,等今后进修了Redux源码今后再做剖析
注:代码基于如今(2016.12.29)React Redux的最新版本(5.0.1)

Connect东西类篇(1)
Connect东西类篇(2)

Components篇

在5.0.1版本中,React Redux供应了两个Components,一个是Provider,别的一个是connectAdvanced
connect应当也算一个,它设置了一些须要的默许值,并挪用、返回connectAdvanced。

Provider

Provider的作用在文档中是这么说的

给下级组件中的connect()供应可用的Redux的store对象。平常状况下,假如根组件没有被<Provider>包裹,那末你就没法运用connect()要领。

假如你对峙不必<Provider>,你也能够给每个须要connect()的组件手动通报store属性。然则我们只发起在unit tests也许非完整React的项目中这么用,不然应当运用<Provider>。

Props

根据文档,属性应当包括store和children:

  • store (Redux Store): The single Redux store in your application.

  • children (ReactElement) The root of your component hierarchy.

先贴一个运用示例:

<Provider store={store}>
  <App />
</Provider>

源码中也对propTypes做了定义(storeShape请看这里)

Provider.propTypes = {
  store: storeShape.isRequired, // store必需含有storeShape (subscribe, dispatch, getState)
  children: PropTypes.element.isRequired // children必需是一个React元素
}

之所以文档中说:给下级组件中的connect()供应可用的Redux的store对象

是由于Provider内里给下级组件在context中增加了store对象,所以下级一切组件都能够拿到store.

export default class Provider extends Component {
  getChildContext() {
    return { store: this.store } // 给下级组件增加store
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  render() {
    return Children.only(this.props.children) // 衬着children
  }
}
Provider.childContextTypes = {
  store: storeShape.isRequired
}

在源码中另有一点是关于hot reload reducers的题目:

let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
  if (didWarnAboutReceivingStore) {
    return
  }
  didWarnAboutReceivingStore = true

  warning(
    '<Provider> does not support changing `store` on the fly. ' +
    'It is most likely that you see this error because you updated to ' +
    'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
    'automatically. See https://github.com/reactjs/react-redux/releases/' +
    'tag/v2.0.0 for the migration instructions.'
  )
}
if (process.env.NODE_ENV !== 'production') {
  Provider.prototype.componentWillReceiveProps = function (nextProps) {
    const { store } = this
    const { store: nextStore } = nextProps

    if (store !== nextStore) {
      warnAboutReceivingStore()
    }
  }
}

好像是React Redux不支持hot reload,根据内里供应的链接,发明hot reload会形成毛病,所以在2.x的时刻举行了修正,运用replaceReducer的要领来初始化App。细致能够看这里,另有这里
我并不知道怎样重现这个,我本身在Hot reload下,修正了reducer和action,然则并没有涌现这个warning…(懵逼脸

connectAdvanced

挪用要领:

connectAdvanced(selectorFactory, options)(MyComponent)

文档这么引见的

把传入的React组件和Redux store举行衔接。这个要领是connect()的基本,然则比拟于connect()缺少了兼并state, props和dispatch的要领。它不包括一些设置的默许值,另有一些便于优化的效果对照。这些一切的事变,都要有挪用者来处理。

这个要领不会修正传入的组件,而是在外面包裹一层,天生一个新的组件。

这个要领须要两个参数:

  1. selectorFactory 也许的花样是这模样的selectorFactory(dispatch, factoryOptions)=>selector(state, ownProps)=>props。每次Redux store也许父组件传入的props发作转变,selector要领就会被挪用,从新盘算新的props。末了的效果props应当是一个plain object,这个props末了会被传给包裹的组件。假如返回的props经由对照(===)和上一次的props是一个对象,那末组件就不会被re-render。所以假如相符前提的话,selector应当返回同一个对象而不是新的对象(就是说,假如props内容没有发作转变,那末就不要从新天生一个新的对象了,直接用之前的对象,如许能够保证===对照返回true)。
    注:在之前的文章中,引见了selectorFactory.js这个文件的内容。这个文件里的selectorFactory主如果被connect()要领引入,并传给connectAdvanced的,算是一个默许的selector。

  2. connectOptions 这个黑白必需参数,中心包括几个参数:

  • getDisplayName(function) 用途不大,主如果用来示意connectAdvanced组件和包括的组件的关联的。比方默许值是name=>'ConnectAdvanced(' + name + ')'。同时假如用connect()的话,那末这个参数会在connect中被掩盖成connect(name)。这个的效果主如果在selectorFactory中考证抛出warning时运用,会被到场到connectOptions一同传给selectorFactory。

  • methodName(string) 示意当前的称号。默许值是’connectAdvanced’,假如运用connect()的话,会被掩盖成’connect’。也是被用在抛出warning的时刻运用

  • renderCountProp(string) 这个主如果用来做优化的时刻运用。假如传入了这个string,那末在传入的props中会多加一个prop(key是renderCountProps的值)。这个值就能够让开辟猎取这个组件从新render的次数,开辟能够根据这个次数来削减过量的re-render.

  • shouldHandleStateChanges(Boolean) 默许值是true。这个值决议了Redux Store State的值发作转变今后,是不是re-render这个组件。假如值为false,那末只要在componentWillReceiveProps(父组件通报的props发作转变)的时刻才会re-render。

  • storeKey(string) 平常不要修正这个。默许值是’store’。这个值示意在context/props内里store的key值。平常只要在含有多个store的时刻,才须要用这个

  • withRef(Boolean) 默许值是false。假如是true的话,父级能够经由过程connectAdvanced中的getWrappedInstance要领来猎取组件的ref。

  • 另有一些其他的options, 这些options都邑经由过程factoryOptions传给selectorFactory举行运用。(假如用的是connect(),那末connect中的options也会被传入)

注:withRef中所谓的父级能够经由过程getWrappedInstance要领来猎取,能够看看下面的代码(从stackoverflow拿的):

class MyDialog extends React.Component {
  save() {
    this.refs.content.getWrappedInstance().save();
  }

  render() {
    return (
      <Dialog action={this.save.bind(this)}>
        <Content ref="content"/>
      </Dialog>);
   }
}

class Content extends React.Component {
  save() { ... }
}

function mapStateToProps(state) { ... }

module.exports = connect(mapStateToProps, null, null, { withRef: true })(Content);

注:由于我对hot reload的运转要领不是很相识。。。所以代码里的hot reload的处所我就不说了。。。

代码太长,而且不庞杂,我直接把诠释写到解释里:

let hotReloadingVersion = 0
export default function connectAdvanced(
  selectorFactory,
  {
    getDisplayName = name => `ConnectAdvanced(${name})`,
    methodName = 'connectAdvanced',
    renderCountProp = undefined,
    shouldHandleStateChanges = true,
    storeKey = 'store',
    withRef = false,
    ...connectOptions
  } = {}
) {
  const subscriptionKey = storeKey + 'Subscription' // subscription的key
  const version = hotReloadingVersion++ // hot reload version

  const contextTypes = {
    [storeKey]: storeShape, // 从Provider那边猎取的store的type
    [subscriptionKey]: PropTypes.instanceOf(Subscription), // 从上级猎取的subscription的type
  }
  const childContextTypes = {
    [subscriptionKey]: PropTypes.instanceOf(Subscription) // 通报给下级的subscription的type
  }

  return function wrapWithConnect(WrappedComponent) {
    // 担任搜检wrappedComponent是不是是function,假如不是抛出非常
    invariant(
      typeof WrappedComponent == 'function',
      `You must pass a component to the function returned by ` +
      `connect. Instead received ${WrappedComponent}`
    )

    const wrappedComponentName = WrappedComponent.displayName
      || WrappedComponent.name
      || 'Component'

    const displayName = getDisplayName(wrappedComponentName) // 用于非常抛出的名字

    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      withRef,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }

    // 假如之前传入的组件叫做wrappedComponent, 这个Connect组件应当叫wrapComponent,用来包裹wrappedComponent用的
    class Connect extends Component {
      constructor(props, context) {
        super(props, context)

        // 初始化一些信息
        this.version = version
        this.state = {}
        this.renderCount = 0
        this.store = this.props[storeKey] || this.context[storeKey] // 猎取store,有props传入的是第一优先级,context中的是第二优先级。
        this.parentSub = props[subscriptionKey] || context[subscriptionKey] // 猎取context

        this.setWrappedInstance = this.setWrappedInstance.bind(this) // 绑定this值,但是不知道有什么用。。。岂非怕他人抢了去?

        // 推断store是不是存在
        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.getState = this.store.getState.bind(this.store); // 定义一个getState要领猎取store内里的state

        this.initSelector()
        this.initSubscription()
      }

      // 把当前的subscription通报给下级组件,下级组件中的connect就能够把监听绑定到这个上面
      getChildContext() {
        return { [subscriptionKey]: 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只要跑过run要领的时刻才会是true
      // run要领只要在Redux store state也许父级传入的props发作转变的时刻,才会运转
      shouldComponentUpdate() {
        return this.selector.shouldComponentUpdate
      }

      // 把一切都回复,这模样能够有助于GC,防止内存走漏
      componentWillUnmount() {
        if (this.subscription) this.subscription.tryUnsubscribe()
        // these are just to guard against extra memory leakage if a parent element doesn't
        // dereference this instance properly, such as an async callback that never finishes
        this.subscription = null
        this.store = null
        this.parentSub = null
        this.selector.run = () => {}
      }

      // 经由过程这个要领,父组件能够获得wrappedComponent的ref
      getWrappedInstance() {
        invariant(withRef,
          `To access the wrapped instance, you need to specify ` +
          `{ withRef: true } in the options argument of the ${methodName}() call.`
        )
        return this.wrappedInstance
      }

      setWrappedInstance(ref) {
        this.wrappedInstance = ref
      }

      initSelector() {
        const { dispatch } = this.store
        const { getState } = this;
        const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions)

        // 注重这里不会举行任何的setState和forceUpdate,也就是说这里不会从新衬着
        // 在这里会纪录上一个props,并和更新后的props举行对照,削减re-render次数
        // 用shouldComponentUpdate来掌握是不是须要re-render
        const selector = this.selector = {
          shouldComponentUpdate: true,
          props: sourceSelector(getState(), this.props),
          run: function runComponentSelector(props) {
            try {
              const nextProps = sourceSelector(getState(), props) // 猎取最新的props
              if (selector.error || nextProps !== selector.props) { // 举行对照,假如props发作了转变才转变props对象,并把可衬着flag设为true
                selector.shouldComponentUpdate = true
                selector.props = nextProps
                selector.error = null
              }
            } catch (error) {
              selector.shouldComponentUpdate = true // 假如有毛病也会把毛病信息衬着到页面上
              selector.error = error
            }
          }
        }
      }

      initSubscription() {
        // 假如组件不根据redux store state举行更新,那末基础不须要监听上级的subscription
        if (shouldHandleStateChanges) {
          // 竖立一个本身的subscription
          const subscription = this.subscription = new Subscription(this.store, this.parentSub)
          const dummyState = {} // 随意的state, 重要就是用来挪用setState来re-render的

          subscription.onStateChange = function onStateChange() {
            this.selector.run(this.props) // 每次redux state发作转变都要从新盘算一遍

            if (!this.selector.shouldComponentUpdate) { // 假如当前组件的props没有发作转变,那末就只关照下级subscription就好
              subscription.notifyNestedSubs()
            } else {
              // 假如发作了转变,那末就在更新完今后,再关照下级
              this.componentDidUpdate = function componentDidUpdate() {
                this.componentDidUpdate = undefined
                subscription.notifyNestedSubs()
              }

              // re-render
              this.setState(dummyState)
            }
          }.bind(this)
        }
      }

      // 推断是不是监听了上级subscription
      isSubscribed() {
        return Boolean(this.subscription) && this.subscription.isSubscribed()
      }

      // 到场过剩的props,注重运用props的影对象举行操纵,防止把ref增加到selector中,形成内存走漏
      addExtraProps(props) {
        if (!withRef && !renderCountProp) return props

        const withExtras = { ...props }
        if (withRef) withExtras.ref = this.setWrappedInstance
        if (renderCountProp) withExtras[renderCountProp] = this.renderCount++
        return withExtras
      }

      render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }
      }
    }

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName
    Connect.childContextTypes = childContextTypes
    Connect.contextTypes = contextTypes
    Connect.propTypes = contextTypes

    if (process.env.NODE_ENV !== 'production') {
      Connect.prototype.componentWillUpdate = function componentWillUpdate() {
        // We are hot reloading!
        if (this.version !== version) {
          this.version = version
          this.initSelector()

          if (this.subscription) this.subscription.tryUnsubscribe()
          this.initSubscription()
          if (shouldHandleStateChanges) this.subscription.trySubscribe()
        }
      }
    }

    return hoistStatics(Connect, WrappedComponent)
  }
}

须要注重的:

  • 在组件中, this.store = this.props[storeKey] || this.context[storeKey]; this.parentSub = props[subscriptionKey] || context[subscriptionKey];, 所以props中的store和subscription都是优先于context的。所以,假如你决议运用差别的store也许subscription,能够在父组件中传入这个值。

connect

connect要领是react-redux最经常使用的要领。这个要领实际上是挪用了connectAdvanced要领,只不过和直接挪用差别的是,这里增加了一些参数的默许值。

而且connectAdvanced要领接收的是selectorFactory作为第一个参数,然则在connect中,分为mapStateToProps, mapDispatchToProps, mergeProps三个参数,并多了一些pure, areStateEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual这些设置。一切的这些多出来的参数都是用于根据selectorFactory.js制作一个简朴的selectorFactory

挪用要领:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

先看两个辅佐用的要领:

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}.`)
  }
}

function strictEqual(a, b) { return a === b }

match之前已在说mapDispatchToProps.js的时刻已提到,这里就不说了。strictEqual就是一个简朴的相对相称的封装。

主题代码是这模样的:

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, {
      // used in error messages
      methodName: 'connect',

       // used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: name => `Connect(${name})`,

      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })
  }
}

export default createConnect()

createConnect要领

个中,createConnect要领是一个factory类的要领,主如果对一些须要的factory举行默许初始化。

export function createConnect({
  connectHOC = connectAdvanced, // connectAdvanced的要领
  mapStateToPropsFactories = defaultMapStateToPropsFactories, // mapStateToProps.js
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, // mapDispatchToProps.js
  mergePropsFactories = defaultMergePropsFactories, // mergeProps.js
  selectorFactory = defaultSelectorFactory // selectorFactory.js
} = {}) {
  // ...
}

由于这个要领也是export的,所以实在由开辟举行挪用,能够自定义本身的factory要领,比方你也许能够这么用:

var myConnect = createConnect({
  connectHOC: undefined, // 运用connectAdvanced
  mapStateToPropsFactories: myMapToStatePropsFactories,
  //.....
});

myConnect(mapStateToProps, mapDispatchToProps, options)(myComponnet);

不过这个要领并没有在文档中提到,多是官方以为,你写这么多factories,还不如用connectAdvanced本身封装一个selectorFactory来的轻易。

connect要领

在内层的connect要领中,除了对几个对照要领举行初始化,主如果针对factories根据传入的参数举行封装、操纵。

function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
  // .......
}

这里的pure参数和equal参数都在前两篇中有细致的形貌(connect东西类1connect东西类2),能够在那边看看。

提一点,项目中能够经由过程根据差别的状况优化…Equal的四个要领来优化项目,削减必不要的从新衬着,由于假如这个*Equal要领考证经由过程,就不会返回新的props对象,而是用本来贮存的props对象(对某些层级比较深的状况来讲,纵然第一层内容雷同,shallowEqual也会返回false,比方shallowEqual({a: {}}, {a: {}})),那末在connectAdvanced中就不会从新衬着。

connect内部完成

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

    return connectHOC(selectorFactory, {
      methodName: 'connect', // 掩盖connectAdvanced中的methodName, 用于毛病信息显现

      getDisplayName: name => `Connect(${name})`, // 掩盖connectAdvanced中的getDisplayName, 用于毛病信息显现

      shouldHandleStateChanges: Boolean(mapStateToProps), // 假如mapStateToProps没有传,那末组件就不须要监听redux store

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })

中心须要提一点,就是shouldHandleStateChanges的这个属性。根据文档中对mapStateToProps的引见,有一句话是:

mapStateToProps 假如这个没有传这个参数,那末组件就不会监听Redux store.

实在缘由很简朴,由于connect中只要mapStateToProps(state, [ownProps])是根据redux store state的转变举行转变的,而像mapDispatchToProps(dispatch, [ownProps])mergeProps(stateProps, dispatchProps, ownProps)都和redux store无关,所以假如mapStateToProps没有传的话,就不须要去监听redux store。

一点总结:

能够怎样去做机能优化?

  • 除了最最基本的shouldComponentUpdate以外,针对Redux React,我们能够经由过程优化areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual四个要领,来确保特别状况下,props的对照更准确。

  • pure只管运用默许的true,只要在内部的衬着会根据除了redux store和父组件传入的props以外的状况举行转变,才会运用false。然则false会形成疏忽上面的对照,每次转变都举行从新衬着

  • mapStateToProps, mapDispatchToProps假如不须要ownProps参数,就不要写到function定义中,削减要领的挪用次数。

  • 假如mapStateToProps不须要的话,就不传也许undefined,不要传noop的function,由于noop要领也会让shouldHandleStateChanges为true,平白让connect多了一个监听要领。

自定义store

之前有提到,react redux是接收自定义store的。也就是说你能够从父组件传入一个store给connect组件,connect组件就会优先运用这个store。然则store必需有肯定的花样,比方内里须要有一个getState要领来猎取state。

加个参数来掌握是不是衬着

在connectAdvanced内里,他们运用了selector.shouldComponentUpdate来掌握是不是须要衬着,然后在React的shouldComponentUpdate内里返回这个属性。这个要领的长处就是,就像一个开关,当须要衬着的时刻再翻开,不须要衬着也许衬着后封闭开关。便于掌握,同时某些不须要衬着的setState,也不会形成衬着。

一个猎取子组件中的component ref的小要领

在看getWrappedInstance要领的时刻,在github上面看到原作者的一个小要领,能够用来猎取子组件中的component。
代码很清楚,只是有的时刻想不到,直接上代码:

class MyComponent extends Component {
  render() {
    return (
      <div>
        <input ref={this.props.inputRef} />
      </div>
    );
  }
}

class ParentComponent extends Component {
  componentDidMount() {
    this.input.focus();
  }

  render() {
    return (
      <MyComponent inputRef={input => this.input = input} />
    )
  }
}

用这类要领,就能够把input的ref直接通报给parentComponent中,在parentComponent中就能够直接对Input举行操纵。这个要领对用connect包裹后的组件一样有用。

    原文作者:Clark
    原文地址: https://segmentfault.com/a/1190000008056652
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞