React Redux: 从文档看源码 - Components篇
注:这篇文章只是讲解React Redux这一层,并不包含Redux部分。Redux有计划去学习,等以后学习了Redux源码以后再做分析 Connect工具类篇(1) Components篇在5.0.1版本中,React Redux提供了两个Components,一个是Provider,另外一个是connectAdvanced。 ProviderProvider的作用在文档中是这么说的
Props根据文档,属性应该包含store和children:
先贴一个使用示例: <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。具体可以看这里,还有这里。 connectAdvanced调用方法: connectAdvanced(selectorFactory,options)(MyComponent) 文档这么介绍的:
这个方法需要两个参数:
注: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,{ 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) } } 需要注意的:
connectconnect方法是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,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,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,options)(myComponnet); 不过这个方法并没有在文档中提到,可能是官方认为,你写这么多factories,还不如用connectAdvanced自己封装一个selectorFactory来的方便。 connect方法在内层的connect方法中,除了对几个对比方法进行初始化,主要是针对factories根据传入的参数进行封装、操作。 function connect( mapStateToProps,...extraOptions } = {} ) { // ....... } 这里的pure参数和equal参数都在前两篇中有详细的描述(connect工具类1, connect工具类2),可以在那里看看。 提一点,项目中可以通过根据不同的情况优化...Equal的四个方法来优化项目,减少必不要的重新渲染,因为如果这个*Equal方法验证通过,就不会返回新的props对象,而是用原来储存的props对象(对某些层级比较深的情况来说,即使第一层内容相同,shallowEqual也会返回false,比如shallowEqual({a: {}},{a: {}})),那么在connectAdvanced中就不会重新渲染。 connect内部实现const initMapStateToProps = match(mapStateToProps,{ methodName: 'connect',// 覆盖connectAdvanced中的methodName,用于错误信息显示 getDisplayName: name => `Connect(${name})`,// 覆盖connectAdvanced中的getDisplayName,用于错误信息显示 shouldHandleStateChanges: Boolean(mapStateToProps),// 如果mapStateToProps没有传,那么组件就不需要监听redux store // passed through to selectorFactory initMapStateToProps,// any extra options args can override defaults of connect or connectAdvanced ...extraOptions }) 中间需要提一点,就是shouldHandleStateChanges的这个属性。根据文档中对mapStateToProps的介绍,有一句话是:
其实原因很简单,由于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包裹后的组件同样有效。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- hybrid的一个实例应用分析
- jsonP小结
- XML存储的名单信息及其相关DTD文档和XSLT文档
- ruby-on-rails – Paperclip – 你如何上传PDF?
- ios – XCode 7如何固定超级视图而不是播放指南
- react-native – 如何在React Native中设置默认字体系?
- ruby-on-rails – 使用less(2.2.1)引发需要therubyracer的错
- new JSONObject()不执行 不报错 解决方法
- 调整大小结束时dojo – dijit / layout / ContentPane?
- ruby-on-rails – 列上的WiceGrid默认过滤器