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

前端之家收集整理的这篇文章主要介绍了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.

先贴一个使用示例:

  1. <Provider store={store}>
  2. <App />
  3. </Provider>

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

  1. Provider.propTypes = {
  2. store: storeShape.isrequired,// store必须含有storeShape (subscribe,dispatch,getState)
  3. children: PropTypes.element.isrequired // children必须是一个React元素
  4. }

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

是因为Provider里面给下级组件在context中添加了store对象,所以下级所有组件都可以拿到store.

  1. export default class Provider extends Component {
  2. getChildContext() {
  3. return { store: this.store } // 给下级组件添加store
  4. }
  5.  
  6. constructor(props,context) {
  7. super(props,context)
  8. this.store = props.store
  9. }
  10.  
  11. render() {
  12. return Children.only(this.props.children) // 渲染children
  13. }
  14. }
  15. Provider.childContextTypes = {
  16. store: storeShape.isrequired
  17. }

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

  1. let didWarnAboutReceivingStore = false
  2. function warnAboutReceivingStore() {
  3. if (didWarnAboutReceivingStore) {
  4. return
  5. }
  6. didWarnAboutReceivingStore = true
  7.  
  8. warning(
  9. '<Provider> does not support changing `store` on the fly. ' +
  10. 'It is most likely that you see this error because you updated to ' +
  11. 'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
  12. 'automatically. See https://github.com/reactjs/react-redux/releases/' +
  13. 'tag/v2.0.0 for the migration instructions.'
  14. )
  15. }
  16. if (process.env.NODE_ENV !== 'production') {
  17. Provider.prototype.componentWillReceiveProps = function (nextProps) {
  18. const { store } = this
  19. const { store: nextStore } = nextProps
  20.  
  21. if (store !== nextStore) {
  22. warnAboutReceivingStore()
  23. }
  24. }
  25. }

好像是React Redux不支持hot reload,根据里面提供的链接,发现hot reload会造成错误,所以在2.x的时候进行了修改,使用replaceReducer的方法来初始化App。具体可以看这里,还有这里
我并不知道怎么重现这个,我自己在Hot reload下,修改了reducer和action,但是并没有出现这个warning...(懵逼脸

connectAdvanced

调用方法

  1. 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拿的):

  1. class MyDialog extends React.Component {
  2. save() {
  3. this.refs.content.getWrappedInstance().save();
  4. }
  5.  
  6. render() {
  7. return (
  8. <Dialog action={this.save.bind(this)}>
  9. <Content ref="content"/>
  10. </Dialog>);
  11. }
  12. }
  13.  
  14. class Content extends React.Component {
  15. save() { ... }
  16. }
  17.  
  18. function mapStateToProps(state) { ... }
  19.  
  20. module.exports = connect(mapStateToProps,null,{ withRef: true })(Content);

注:由于我对hot reload的运行方法不是很了解。。。所以代码里的hot reload的地方我就不说了。。。

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

  1. let hotReloadingVersion = 0
  2. export default function connectAdvanced(
  3. selectorFactory,{
  4. getDisplayName = name => `ConnectAdvanced(${name})`,methodName = 'connectAdvanced',renderCountProp = undefined,shouldHandleStateChanges = true,storeKey = 'store',withRef = false,...connectOptions
  5. } = {}
  6. ) {
  7. const subscriptionKey = storeKey + 'Subscription' // subscription的key
  8. const version = hotReloadingVersion++ // hot reload version
  9.  
  10. const contextTypes = {
  11. [storeKey]: storeShape,// 从Provider那里获取的store的type
  12. [subscriptionKey]: PropTypes.instanceOf(Subscription),// 从上级获取的subscription的type
  13. }
  14. const childContextTypes = {
  15. [subscriptionKey]: PropTypes.instanceOf(Subscription) // 传递给下级的subscription的type
  16. }
  17.  
  18. return function wrapWithConnect(WrappedComponent) {
  19. // 负责检查wrappedComponent是否是function,如果不是抛出异常
  20. invariant(
  21. typeof WrappedComponent == 'function',`You must pass a component to the function returned by ` +
  22. `connect. Instead received ${WrappedComponent}`
  23. )
  24.  
  25. const wrappedComponentName = WrappedComponent.displayName
  26. || WrappedComponent.name
  27. || 'Component'
  28.  
  29. const displayName = getDisplayName(wrappedComponentName) // 用于异常抛出的名字
  30.  
  31. const selectorFactoryOptions = {
  32. ...connectOptions,getDisplayName,methodName,renderCountProp,shouldHandleStateChanges,storeKey,withRef,displayName,wrappedComponentName,WrappedComponent
  33. }
  34.  
  35. // 如果之前传入的组件叫做wrappedComponent, 这个Connect组件应该叫wrapComponent,用来包裹wrappedComponent用的
  36. class Connect extends Component {
  37. constructor(props,context) {
  38. super(props,context)
  39.  
  40. // 初始化一些信息
  41. this.version = version
  42. this.state = {}
  43. this.renderCount = 0
  44. this.store = this.props[storeKey] || this.context[storeKey] // 获取store,有props传入的是第一优先级,context中的是第二优先级。
  45. this.parentSub = props[subscriptionKey] || context[subscriptionKey] // 获取context
  46.  
  47. this.setWrappedInstance = this.setWrappedInstance.bind(this) // 绑定this值,然而不知道有什么用。。。难道怕别人抢了去?
  48.  
  49. // 判断store是否存在
  50. invariant(this.store,`Could not find "${storeKey}" in either the context or ` +
  51. `props of "${displayName}". ` +
  52. `Either wrap the root component in a <Provider>,` +
  53. `or explicitly pass "${storeKey}" as a prop to "${displayName}".`
  54. )
  55.  
  56. this.getState = this.store.getState.bind(this.store); // 定义一个getState方法获取store里面的state
  57.  
  58. this.initSelector()
  59. this.initSubscription()
  60. }
  61.  
  62. // 把当前的subscription传递给下级组件,下级组件中的connect就可以把监听绑定到这个上面
  63. getChildContext() {
  64. return { [subscriptionKey]: this.subscription }
  65. }
  66.  
  67. componentDidMount() {
  68. if (!shouldHandleStateChanges) return
  69.  
  70. this.subscription.trySubscribe()
  71. this.selector.run(this.props)
  72. if (this.selector.shouldComponentUpdate) this.forceUpdate()
  73. }
  74.  
  75. componentWillReceiveProps(nextProps) {
  76. this.selector.run(nextProps)
  77. }
  78.  
  79. // shouldComponentUpdate只有跑过run方法的时候才会是true
  80. // run方法只有在Redux store state或者父级传入的props发生改变的时候,才会运行
  81. shouldComponentUpdate() {
  82. return this.selector.shouldComponentUpdate
  83. }
  84.  
  85. // 把一切都复原,这样子可以有助于GC,避免内存泄漏
  86. componentWillUnmount() {
  87. if (this.subscription) this.subscription.tryUnsubscribe()
  88. // these are just to guard against extra memory leakage if a parent element doesn't
  89. // dereference this instance properly,such as an async callback that never finishes
  90. this.subscription = null
  91. this.store = null
  92. this.parentSub = null
  93. this.selector.run = () => {}
  94. }
  95.  
  96. // 通过这个方法,父组件可以获得wrappedComponent的ref
  97. getWrappedInstance() {
  98. invariant(withRef,`To access the wrapped instance,you need to specify ` +
  99. `{ withRef: true } in the options argument of the ${methodName}() call.`
  100. )
  101. return this.wrappedInstance
  102. }
  103.  
  104. setWrappedInstance(ref) {
  105. this.wrappedInstance = ref
  106. }
  107.  
  108. initSelector() {
  109. const { dispatch } = this.store
  110. const { getState } = this;
  111. const sourceSelector = selectorFactory(dispatch,selectorFactoryOptions)
  112.  
  113. // 注意这里不会进行任何的setState和forceUpdate,也就是说这里不会重新渲染
  114. // 在这里会记录上一个props,并和更新后的props进行对比,减少re-render次数
  115. // 用shouldComponentUpdate来控制是否需要re-render
  116. const selector = this.selector = {
  117. shouldComponentUpdate: true,props: sourceSelector(getState(),this.props),run: function runComponentSelector(props) {
  118. try {
  119. const nextProps = sourceSelector(getState(),props) // 获取最新的props
  120. if (selector.error || nextProps !== selector.props) { // 进行对比,如果props发生了改变才改变props对象,并把可渲染flag设为true
  121. selector.shouldComponentUpdate = true
  122. selector.props = nextProps
  123. selector.error = null
  124. }
  125. } catch (error) {
  126. selector.shouldComponentUpdate = true // 如果有错误也会把错误信息渲染到页面
  127. selector.error = error
  128. }
  129. }
  130. }
  131. }
  132.  
  133. initSubscription() {
  134. // 如果组件不依据redux store state进行更新,那么根本不需要监听上级的subscription
  135. if (shouldHandleStateChanges) {
  136. // 建立一个自己的subscription
  137. const subscription = this.subscription = new Subscription(this.store,this.parentSub)
  138. const dummyState = {} // 随便的state,主要就是用来调用setState来re-render的
  139.  
  140. subscription.onStateChange = function onStateChange() {
  141. this.selector.run(this.props) // 每次redux state发生改变都要重新计算一遍
  142.  
  143. if (!this.selector.shouldComponentUpdate) { // 如果当前组件的props没有发生改变,那么就只通知下级subscription就好
  144. subscription.notifyNestedSubs()
  145. } else {
  146. // 如果发生了改变,那么就在更新完以后,再通知下级
  147. this.componentDidUpdate = function componentDidUpdate() {
  148. this.componentDidUpdate = undefined
  149. subscription.notifyNestedSubs()
  150. }
  151.  
  152. // re-render
  153. this.setState(dummyState)
  154. }
  155. }.bind(this)
  156. }
  157. }
  158.  
  159. // 判断是否监听了上级subscription
  160. isSubscribed() {
  161. return Boolean(this.subscription) && this.subscription.isSubscribed()
  162. }
  163.  
  164. // 加入多余的props,注意使用props的影对象进行操作,避免把ref添加到selector中,造成内存泄漏
  165. addExtraProps(props) {
  166. if (!withRef && !renderCountProp) return props
  167.  
  168. const withExtras = { ...props }
  169. if (withRef) withExtras.ref = this.setWrappedInstance
  170. if (renderCountProp) withExtras[renderCountProp] = this.renderCount++
  171. return withExtras
  172. }
  173.  
  174. render() {
  175. const selector = this.selector
  176. selector.shouldComponentUpdate = false
  177.  
  178. if (selector.error) {
  179. throw selector.error
  180. } else {
  181. return createElement(WrappedComponent,this.addExtraProps(selector.props))
  182. }
  183. }
  184. }
  185.  
  186. Connect.WrappedComponent = WrappedComponent
  187. Connect.displayName = displayName
  188. Connect.childContextTypes = childContextTypes
  189. Connect.contextTypes = contextTypes
  190. Connect.propTypes = contextTypes
  191.  
  192. if (process.env.NODE_ENV !== 'production') {
  193. Connect.prototype.componentWillUpdate = function componentWillUpdate() {
  194. // We are hot reloading!
  195. if (this.version !== version) {
  196. this.version = version
  197. this.initSelector()
  198.  
  199. if (this.subscription) this.subscription.tryUnsubscribe()
  200. this.initSubscription()
  201. if (shouldHandleStateChanges) this.subscription.trySubscribe()
  202. }
  203. }
  204. }
  205.  
  206. return hoistStatics(Connect,WrappedComponent)
  207. }
  208. }

需要注意的:

  • 在组件中,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

调用方法

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

先看两个辅助用的方法

  1. function match(arg,factories,name) {
  2. for (let i = factories.length - 1; i >= 0; i--) {
  3. const result = factories[i](arg)
  4. if (result) return result
  5. }
  6.  
  7. return (dispatch,options) => {
  8. throw new Error(`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${options.wrappedComponentName}.`)
  9. }
  10. }
  11.  
  12. function strictEqual(a,b) { return a === b }

match之前已经在说mapDispatchToProps.js的时候已经提到,这里就不说了。strictEqual就是一个简单的绝对相等的封装。

主题代码是这样子的:

  1. export function createConnect({
  2. connectHOC = connectAdvanced,mapStateToPropsFactories = defaultMapStateToPropsFactories,mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,mergePropsFactories = defaultMergePropsFactories,selectorFactory = defaultSelectorFactory
  3. } = {}) {
  4. return function connect(
  5. mapStateToProps,mergeProps,{
  6. pure = true,areStatesEqual = strictEqual,areOwnPropsEqual = shallowEqual,areStatePropsEqual = shallowEqual,areMergedPropsEqual = shallowEqual,...extraOptions
  7. } = {}
  8. ) {
  9. const initMapStateToProps = match(mapStateToProps,mapStateToPropsFactories,'mapStateToProps')
  10. const initMapDispatchToProps = match(mapDispatchToProps,mapDispatchToPropsFactories,'mapDispatchToProps')
  11. const initMergeProps = match(mergeProps,mergePropsFactories,'mergeProps')
  12.  
  13. return connectHOC(selectorFactory,{
  14. // used in error messages
  15. methodName: 'connect',// used to compute Connect's displayName from the wrapped component's displayName.
  16. getDisplayName: name => `Connect(${name})`,// if mapStateToProps is falsy,the Connect component doesn't subscribe to store state changes
  17. shouldHandleStateChanges: Boolean(mapStateToProps),// passed through to selectorFactory
  18. initMapStateToProps,initMapDispatchToProps,initMergeProps,pure,areStatesEqual,areMergedPropsEqual,// any extra options args can override defaults of connect or connectAdvanced
  19. ...extraOptions
  20. })
  21. }
  22. }
  23.  
  24. export default createConnect()

createConnect方法

其中,createConnect方法是一个factory类的方法,主要是对一些需要的factory进行默认初始化。

  1. export function createConnect({
  2. connectHOC = connectAdvanced,// connectAdvanced的方法
  3. mapStateToPropsFactories = defaultMapStateToPropsFactories,// mapStateToProps.js
  4. mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,// mapDispatchToProps.js
  5. mergePropsFactories = defaultMergePropsFactories,// mergeProps.js
  6. selectorFactory = defaultSelectorFactory // selectorFactory.js
  7. } = {}) {
  8. // ...
  9. }

由于这个方法也是export的,所以其实由开发进行调用,可以自定义自己的factory方法,比如你或许可以这么用:

  1. var myConnect = createConnect({
  2. connectHOC: undefined,// 使用connectAdvanced
  3. mapStateToPropsFactories: myMapToStatePropsFactories,//.....
  4. });
  5.  
  6. myConnect(mapStateToProps,options)(myComponnet);

不过这个方法并没有在文档中提到,可能是官方认为,你写这么多factories,还不如用connectAdvanced自己封装一个selectorFactory来的方便。

connect方法

在内层的connect方法中,除了对几个对比方法进行初始化,主要是针对factories根据传入的参数进行封装、操作。

  1. function connect(
  2. mapStateToProps,...extraOptions
  3. } = {}
  4. ) {
  5. // .......
  6. }

这里的pure参数和equal参数都在前两篇中有详细的描述(connect工具类1connect工具类2),可以在那里看看。

提一点,项目中可以通过根据不同的情况优化...Equal的四个方法来优化项目,减少必不要的重新渲染,因为如果这个*Equal方法验证通过,就不会返回新的props对象,而是用原来储存的props对象(对某些层级比较深的情况来说,即使第一层内容相同,shallowEqual也会返回false,比如shallowEqual({a: {}},{a: {}})),那么在connectAdvanced中就不会重新渲染。

connect内部实现

  1. const initMapStateToProps = match(mapStateToProps,{
  2. methodName: 'connect',// 覆盖connectAdvanced中的methodName,用于错误信息显示
  3.  
  4. getDisplayName: name => `Connect(${name})`,// 覆盖connectAdvanced中的getDisplayName,用于错误信息显示
  5.  
  6. shouldHandleStateChanges: Boolean(mapStateToProps),// 如果mapStateToProps没有传,那么组件就不需要监听redux store
  7.  
  8. // passed through to selectorFactory
  9. initMapStateToProps,// any extra options args can override defaults of connect or connectAdvanced
  10. ...extraOptions
  11. })

中间需要提一点,就是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。
代码很清晰,只是有的时候想不到,直接上代码

  1. class MyComponent extends Component {
  2. render() {
  3. return (
  4. <div>
  5. <input ref={this.props.inputRef} />
  6. </div>
  7. );
  8. }
  9. }
  10.  
  11. class ParentComponent extends Component {
  12. componentDidMount() {
  13. this.input.focus();
  14. }
  15.  
  16. render() {
  17. return (
  18. <MyComponent inputRef={input => this.input = input} />
  19. )
  20. }
  21. }

用这种方法,就可以把input的ref直接传递给parentComponent中,在parentComponent中就可以直接对Input进行操作。这个方法对用connect包裹后的组件同样有效。

猜你在找的React相关文章