You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
classConnectextendsComponent{constructor(props,context){super(props,context)this.version=versionthis.state={}this.renderCount=0this.store=props[storeKey]||context[storeKey]this.propsMode=Boolean(props[storeKey])this.setWrappedInstance=this.setWrappedInstance.bind(this)this.initSelector()this.initSubscription()}getChildContext(){constsubscription=this.propsMode ? null : this.subscriptionreturn{[subscriptionKey]: subscription||this.context[subscriptionKey]}}componentDidMount(){if(!shouldHandleStateChanges)returnthis.subscription.trySubscribe()this.selector.run(this.props)if(this.selector.shouldComponentUpdate)this.forceUpdate()}componentWillReceiveProps(nextProps){this.selector.run(nextProps)}shouldComponentUpdate(){returnthis.selector.shouldComponentUpdate}componentWillUnmount(){if(this.subscription)this.subscription.tryUnsubscribe()this.subscription=nullthis.notifyNestedSubs=noopthis.store=nullthis.selector.run=noopthis.selector.shouldComponentUpdate=false}getWrappedInstance(){returnthis.wrappedInstance}setWrappedInstance(ref){this.wrappedInstance=ref}initSelector(){constsourceSelector=selectorFactory(this.store.dispatch,selectorFactoryOptions)this.selector=makeSelectorStateful(sourceSelector,this.store)this.selector.run(this.props)}initSubscription(){if(!shouldHandleStateChanges)returnconstparentSub=(this.propsMode ? this.props : this.context)[subscriptionKey]this.subscription=newSubscription(this.store,parentSub,this.onStateChange.bind(this))this.notifyNestedSubs=this.subscription.notifyNestedSubs.bind(this.subscription)}onStateChange(){this.selector.run(this.props)if(!this.selector.shouldComponentUpdate){this.notifyNestedSubs()}else{this.componentDidUpdate=this.notifyNestedSubsOnComponentDidUpdatethis.setState(dummyState)}}notifyNestedSubsOnComponentDidUpdate(){this.componentDidUpdate=undefinedthis.notifyNestedSubs()}isSubscribed(){returnBoolean(this.subscription)&&this.subscription.isSubscribed()}addExtraProps(props){if(!withRef&&!renderCountProp&&!(this.propsMode&&this.subscription))returnpropsconstwithExtras={ ...props}if(withRef)withExtras.ref=this.setWrappedInstanceif(renderCountProp)withExtras[renderCountProp]=this.renderCount++if(this.propsMode&&this.subscription)withExtras[subscriptionKey]=this.subscriptionreturnwithExtras}render(){constselector=this.selectorselector.shouldComponentUpdate=falseif(selector.error){throwselector.error}else{returncreateElement(WrappedComponent,this.addExtraProps(selector.props))}}}Connect.WrappedComponent=WrappedComponentConnect.displayName=displayNameConnect.childContextTypes=childContextTypesConnect.contextTypes=contextTypesConnect.propTypes=contextTypesif(process.env.NODE_ENV!=='production'){Connect.prototype.componentWillUpdate=functioncomponentWillUpdate(){// We are hot reloading!if(this.version!==version){this.version=versionthis.initSelector()if(this.subscription)this.subscription.tryUnsubscribe()this.initSubscription()if(shouldHandleStateChanges)this.subscription.trySubscribe()}}}
//Connect类方法initSelector(){constsourceSelector=selectorFactory(this.store.dispatch,selectorFactoryOptions)this.selector=makeSelectorStateful(sourceSelector,this.store)this.selector.run(this.props)}//connectAdvanced外定义的函数functionmakeSelectorStateful(sourceSelector,store){// wrap the selector in an object that tracks its results between runs.constselector={run: functionrunComponentSelector(props){try{constnextProps=sourceSelector(store.getState(),props)if(nextProps!==selector.props||selector.error){selector.shouldComponentUpdate=trueselector.props=nextPropsselector.error=null}}catch(error){selector.shouldComponentUpdate=trueselector.error=error}}}returnselector}
if(process.env.NODE_ENV!=='production'){Connect.prototype.componentWillUpdate=functioncomponentWillUpdate(){// We are hot reloading!if(this.version!==version){this.version=versionthis.initSelector()if(this.subscription)this.subscription.tryUnsubscribe()this.initSubscription()if(shouldHandleStateChanges)this.subscription.trySubscribe()}}}
import{Component,Children}from'react'importPropTypesfrom'prop-types'import{storeShape,subscriptionShape}from'../utils/PropTypes'importwarningfrom'../utils/warning'letdidWarnAboutReceivingStore=falsefunctionwarnAboutReceivingStore(){if(didWarnAboutReceivingStore){return}didWarnAboutReceivingStore=truewarning('<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.')}exportfunctioncreateProvider(storeKey='store',subKey){constsubscriptionKey=subKey||`${storeKey}Subscription`classProviderextendsComponent{getChildContext(){return{[storeKey]: this[storeKey],[subscriptionKey]: null}}constructor(props,context){super(props,context)this[storeKey]=props.store;}render(){returnChildren.only(this.props.children)}}if(process.env.NODE_ENV!=='production'){Provider.prototype.componentWillReceiveProps=function(nextProps){if(this[storeKey]!==nextProps.store){warnAboutReceivingStore()}}}Provider.propTypes={store: storeShape.isRequired,children: PropTypes.element.isRequired,}Provider.childContextTypes={[storeKey]: storeShape.isRequired,[subscriptionKey]: subscriptionShape,}Provider.displayName='Provider'returnProvider}exportdefaultcreateProvider()
转眼间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)1. [getDisplayName] (Function): 用来计算connector component的displayName。
2. [methodName] (String) 用来在错误信息中显示,默认值为
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
仅支持单个子组件。最后欢迎大家关注我的掘金账号或者博客,不足之处,欢迎指正。
The text was updated successfully, but these errors were encountered: