注:这篇文章只是讲解
React Redux
这一层,并不包含
Redux
部分。Redux有计划去学习,等以后学习了Redux源码以后再做分析
注:代码基于现在(2016.12.29)React Redux的最新版本(5.0.1)
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的方法。它不包含一些配置的默认值,还有一些便于优化的结果对比。这些所有的事情,都要有调用者来解决。
这个方法不会修改传入的组件,而是在外面包裹一层,生成一个新的组件。
这个方法需要两个参数:
-
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。 -
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工具类1
,
connect工具类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包裹后的组件同样有效。