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()
}
}
}
转眼间2017年已经过了一半了,看到之前有人问是否完成了自己半年的计划,答案是:当然没有啦。感觉自己现在对技术产生了敬畏,因为要学习的知识是在是太多了,而自己的时间和精力却很难达到目标,目前处在比较焦虑的状态。自己是年初进入掘金的,半年内虽然文章的阅读量不错但是关注度太低,半年就混了40个关注,说来真是惭愧。 扯远了,我们言归正传,上次的文章Redux:百行代码千行文档解释了Redux内部的运作原理。但是我们在React中很少会直接搭配使用Redux,而是通过React-Redux绑定React与Redux。这篇文章我们我们将了解React-Redux其中的奥秘。在阅读之前希望你有React-Redux的使用经验,否则这篇文章可能不太适合你。 首先我们可以看看React-Redux的源码目录结构,大致看一下,做到心里有数。
. ├── components │ ├── Provider.js │ └── connectAdvanced.js ├── connect │ ├── connect.js │ ├── mapDispatchToProps.js │ ├── mapStateToProps.js │ ├── mergeProps.js │ ├── selectorFactory.js │ ├── verifySubselectors.js │ └── wrapMapToProps.js ├── index.js └── utils ├── PropTypes.js ├── Subscription.js ├── shallowEqual.js ├── verifyPlainObject.js ├── warning.js └── wrapActionCreators.js
首先来看一下index.js:
我们可以看出来,React-Redux对外提供的API有四个:
Provider
、createProvider
,connectAdvanced
,connect
。我们将从connectAdvanced
开始介绍。connectAdvanced
其实我在看React-Redux源码之前都不知道有这个API,为了方便后面的源码理解,我们介绍一下
connectAdvanced
:connectAdvanced(selectorFactory, [connectOptions])
connectAdvanced
用来连接组件到Redux的store上。是connect
函数的基础,但并没有规定如何将state
、props
、dispatch
处理传入最终的props
中。connectAdvanced
并没有对产生的props做缓存来优化性能,都留给了调用者去实现。connectAdvanced
并没有修改传入的组件类,而是返回一个新的、连接到store的组件类。参数:
selector
函数(在每次实例的构造函数中)。selector
函数在每次connector component
需要计算新的props(在组件传递新的props和store中数据发生改变时会计算新的props)都会被调用。selector
函数会返回纯对象(plain object),这个对象会作为props传递给被包裹的组件(WrappedComponent)connectAdvanced
3. [renderCountProp] (String): 如果定义了这个属性,以该属性命名的值会被以props传递给包裹组件。该值是组件渲染的次数,可以追踪不必要的渲染。 4. [shouldHandleStateChanges] (Boolean): 控制conntector
组件是否应该订阅redux store中的state变化。 5. [storeKey] (String): 你想要从context和props获得store的key值,只有在需要多个store的情况下才会用到(当然,这并不是明智的选择)。默认是store
。 6. [withRef] (Boolean): 如果是true
,存储被包裹组件的实例,并可以通过函数getWrappedInstance
获得该实例,默认值为false
。 7. 在connectOptions
中额外的属性会被传递给selectorFactory
函数的factoryOptions
属性。返回:
函数返回一个高阶组件,该高阶组件将从store的state中构建的props传递给被包裹组件。
例如:
讲了这么多,我们看看
connectAdvanced
是如何实现的,一开始本来想把所有的代码都列出来,但是感觉直接列出200多行的代码看着确实不方便,所以我们还是一部分一部分介绍:函数接受两个参数:
selectorFactory
与connectOptions
(可选),返回一个高阶组件wrapWithConnect
(以属性代理方式实现),高阶组件中创建了组件类Connect
, 最后返回了hoistStatics(Connect, WrappedComponent)
。其中hoistStatics
来源于:作用是将
WrappedComponent
中的非React特定的静态属性(例如propTypes
就是React的特定静态属性)赋值到Connect
。作用有点类似于Object.assign
,但是仅复制非React特定的静态属性。其实对于React-Redux之所以可以使得
Provider
中的任何子组件访问到Redux中的store
并订阅store
,无非是利用context
,使得所有子组件都能访问store
。更进一步,我们看看高阶组件时如何实现:上面的代码并没有什么难以理解的,
connectAdvanced
中定义了subscriptionKey
、version
以及为Connect
组件定义的contextTypes
与childContextTypes
(不了解context
的同学可以看这里)。在高阶组件中所作的就是定义组装了selectorFactory
所用到的参数selectorFactoryOptions
。接下来介绍最重要的组件类Connect
:我们首先来看构造函数:
首先我们先看看用来初始化
selector
的initSelector
函数:我们知道,
selector
的主要作用是用来从store
中的state
和ownProps
中计算新的props,并返回纯对象(plain object),这个对象会作为props传递给被包裹的组件(WrappedComponent)。在initSelector
中,首先调用selectorFactory
从而初始化sourceSelector
,我们并不会直接调用sourceSelector
,而是为了程序的健壮,通过将sourceSelector
作为参数调用makeSelectorStateful
,返回更加安全的selector
。从此之后,我们想要生成新的props
只需要调用selector.run
函数。在selector.run
函数中对sourceSelector
的异常做了处理,并用sourceSelector.error
记录是否存在异常。sourceSelector.shouldComponentUpdate
用来根据前后两次返回的props
是否相同,从而记录是否应该刷新组件,这就为后期的性能提升留出了空间,只要在前后数据相同时,我们就返回同一个对象,使得shouldComponentUpdate
为false
,就可以避免不必要的刷新,当然这不是我们selector
的职责,而是sourceSelector
所需要做的。每次返回的新的props
都会记录在selector.props
以备后用。再看
initSubscription
函数之前,我们需要先了解一下Subscription
类:首先我们先看函数
createListenerCollection
,这边的代码逻辑和redux
中的listener
逻辑一致,可以了解一下之前的文章Redux:百行代码千行文档。createListenerCollection
通过闭包的方式存储current
和next
,然后返回作为对外接口,分别用来清除当前存储的listener、通知、订阅,其目的就是实现一个监听者模式。然后类
Subscription
封装了订阅的逻辑,Subscription
根据构造函数中是否传入了父级的订阅类Subscription实例parentSub
,订阅方法trySubscribe
会有不同的行为。首先看看parentSub
的来源:我们知道
Provider
的主要作用就是通过context
向子组件提供store
,而在conectAdvanced
函数的参数connectOptions
中的storeKey
是用来区分从context和props获得store的key值,只有在需要多个store的情况下才会用到,当然这并不是什么好事,毕竟Redux追求的是单个store
。例如你设置了storeKey
为otherStore
,那么就可以通过给wrapWithConnect
返回的组件添加属性otherStore
,从而注入新的store
。 下面我们区分几种情况:情况1:
如果
Provider
中的子组件连接到Redux的store,并且祖先组件都没有连接到Redux的store,也就是说是当前组件是通往根节点的路径中第一个连接到Redux的store的组件,这时候直接可以使用Redux的store
中的subscribe
方法去订阅store
的改变。对应于的代码是tryUnsubscribe
方法中的情况2:
如果当前组件并不是通往根节点的路径中第一个连接到Redux的store的组件,也就是父组件中存在已经连接到Redux的store的组件。这时候,必须要保证下层的组件响应
store
改变的函数调用必须晚于父级组件响应store
的函数调用,例如在图中红色的组件在store更新时是晚于黑色的组件的。代码中是如下实现的,在父级组件中,如下:因此在子组件(红色)中就可以通过
context
获得父组件的subscription(也就是parentSub)。这样在执行tryUnsubscribe
时对应于这样我们将子组件处理store中state的函数添加到
parentSub
中的listener
中。这样在父组件更新结束后,就可以调用this.notifyNestedSubs()
。这样就保证了更新顺序,子组件永远在父组件更新之后。情况3:
如上图所示,右边的组件是通过属性prop的方式传入了
store
,那么组件中的this.store
中的值就是通过以props传入的store
。假如祖先元素没有连接到store
的组件,那么当前组件中parentSub
值就为空。所以订阅的方式就是以props中的store
:情况4:
如上图所示,右下方的组件的父组件(紫色)是通过props传入
store
的,那么在父组件(紫色)中有父组件对子组件暴露
context
,其中context
中的subscriptionKey
属性值为this.context[subscriptionKey]
,要么是null
,要么是祖先元素中非props
方式传入store
的组件的subscription
。也就是说以props传入的store
的父组件不会影响子组件的订阅store
。感觉说的太过于抽象,我们举个例子:在上面这个例子中,如果发出
dispatch
更新store1,组件A和组件C都会刷新,组件B不会刷新。 讨论了这么多,我们可以看一下initSubscription
的实现方式:如果当前的store不是以props的方式传入的,那么
parentSub
是this.context[subscriptionKey]
。如果是以props的方式传入的,若显式地给组件以props的方式传入subscription时,parentSub
值为this.props.subscription
。需要注意的是,我们在initSubscription
中拷贝了当前this.subscription
中的notifyNestedSubs
方法,目的是防止在notify
循环过程中组件卸载,使得this.subscription
为null
。我们在组件卸载时,会将值赋值为一个名为no-loop
的空函数,避免出错。当然这并不是唯一的解决方法。 接下我们可以看一下Connect
组件中主要生命周期函数:组件在
did mount
时会根据可选参数shouldHandleStateChanges
选择是否订阅store
的state
改变。组件在接受props时,会使用selector计算新的props并执行相应的声明周期。shouldComponentUpdate
会根据this.selector
存储的值shouldComponentUpdate
来判断是否需要刷新组件。在组件will mount
时会做相应的清理,防止内存泄露。接着我们介绍其他的类方法:
getWrappedInstance
与setWrappedInstance
在可选参数withRef
为true时,获取或者存储被包裹组件的实例(ref)。onStateChange
函数是store发生改变的回调函数,当回调onStateChange
方法时,会通过selector计算新的props,如果计算selcetor的结果中shouldComponentUpdate
为false
,表示不需要刷新当前组件仅需要通知子组件更新。如果shouldComponentUpdate
为true
,会通过设置this.setState({})
来刷新组件,并使得在组件更新结束之后,通知子组件更新。addExtraProps
函数主要用作为selector
计算出的props
增加新的属性。例如,ref
属性用来绑定回调存储组件实例的函数setWrappedInstance
,renderCountProp
为当前组件属性刷新的次数,subscriptionKey
用来传递当前connect
中的subscription
。render
函数其实就是高阶函数中的属性代理,首先将shouldComponentUpdate
置回false
,然后根据selector
中的计算过程是否存在error
,如果存在error
就抛出,否则执行如果你对上面语句不太熟悉,其实上面代码等同于:
其实所谓的
jsx
也无非是createElement
语法糖,所有的jsx
的语法都会被编译成React.createElement
,所以哪怕你的代码中没有显式的用到React
,只要有jsx
语法,就必须存在React
。React-Redux在生产环境下是不支持热重载的,只有在开发环境下提供这个功能。在开发环境中,组件在
will update
时会根据this.version
与version
去判断,如果两者不一样,则初始化selector
,取消之前的订阅并重新订阅新的subscription
。Provider
首先我们看看函数
createProvider
,createProvider
函数的主要作用就是定制Provider
,我们知道Provider
的主要作用是使得其所有子组件可以通过context
访问到Redux的store
。我们看到createProvider
返回了类Provider
,而类Provider
的getChildContext
函数返回了{ [storeKey]: this[storeKey], [subscriptionKey]: null }
,使得所有子组件都能访问到store
。需要注意的是,要想使得子组件访问到context
必须同时定义两点:getChildContext
函数与static childContextTypes = {}
。并且我们知道Redux 2.x 与React-Redux 2.x不再支持热重载的reducer
,所以在非生产环境下,我们会为Provider
添加生命周期函数componentWillReceiveProps
,如果store
的值发生了变化,就会在提供警告提示。Provider
的render
函数中返回了Children.only(this.props.children)
。Children
是React
提供的处理组件中this.props.children
的工具包(utilities)返回仅有的一个子元素,否则(没有子元素或超过一个子元素)报错且不渲染任何东西。所以Provider
仅支持单个子组件。 最后欢迎大家关注我的掘金账号或者博客,不足之处,欢迎指正。