转眼间2017年已通过了一半了,看到以前有人问是否完成了本身半年的计划,答案是:固然没有啦。感受本身如今对技术产生了敬畏,由于要学习的知识是在是太多了,而本身的时间和精力却很难达到目标,目前处在比较焦虑的状态。本身是年初进入掘金的,半年内虽然文章的阅读量不错可是关注度过低,半年就混了40个关注,说来真是惭愧。
扯远了,咱们言归正传,上次的文章Redux:百行代码千行文档解释了Redux内部的运做原理。可是咱们在React中不多会直接搭配使用Redux,而是经过React-Redux绑定React与Redux。这篇文章咱们咱们将了解React-Redux其中的奥秘。在阅读以前但愿你有React-Redux的使用经验,不然这篇文章可能不太适合你。
首先咱们能够看看React-Redux的源码目录结构,大体看一下,作到内心有数。javascript
.
├── components
│ ├── Provider.js
│ └── connectAdvanced.js
├── connect
│ ├── connect.js
│ ├── mapDispatchToProps.js
│ ├── mapStateToProps.js
│ ├── mergeProps.js
│ ├── selectorFactory.js
│ ├── verifySubselectors.js
│ └── wrapMapToProps.js
├── index.js
└── utilsjava
├── PropTypes.js ├── Subscription.js ├── shallowEqual.js ├── verifyPlainObject.js ├── warning.js └── wrapActionCreators.js
首先来看一下index.js:react
import Provider, { createProvider } from './components/Provider' import connectAdvanced from './components/connectAdvanced' import connect from './connect/connect' export { Provider, createProvider, connectAdvanced, connect }
咱们能够看出来,React-Redux对外提供的API有四个:Provider
、createProvider
,connectAdvanced
,connect
。咱们将从connectAdvanced
开始介绍。git
其实我在看React-Redux源码以前都不知道有这个API,为了方便后面的源码理解,咱们介绍一下connectAdvanced
:
connectAdvanced(selectorFactory, [connectOptions])
github
connectAdvanced
用来链接组件到Redux的store上。是connect
函数的基础,但并无规定如何将state
、props
、dispatch
处理传入最终的props
中。connectAdvanced
并无对产生的props作缓存来优化性能,都留给了调用者去实现。connectAdvanced
并无修改传入的组件类,而是返回一个新的、链接到store的组件类。redux
参数:缓存
selectorFactory(dispatch, factoryOptions): selector(state, ownProps): props (Function),用来初始化selector
函数(在每次实例的构造函数中)。selector
函数在每次connector component
须要计算新的props(在组件传递新的props和store中数据发生改变时会计算新的props)都会被调用。selector
函数会返回纯对象(plain object),这个对象会做为props传递给被包裹的组件(WrappedComponent)安全
[connectOptions] (Object) 若是定义了该参数,用来进一步定制connector:
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
属性。app
返回:
函数返回一个高阶组件,该高阶组件将从store的state中构建的props传递给被包裹组件。
例如:
// 按照用户信息选择性传入todos的部分信息 import * as actionCreators from './actionCreators' import { bindActionCreators } from 'redux' function selectorFactory(dispatch) { let ownProps = {} let result = {} const actions = bindActionCreators(actionCreators, dispatch) const addTodo = (text) => actions.addTodo(ownProps.userId, text) return (nextState, nextOwnProps) => { const todos = nextState.todos[nextProps.userId] const nextResult = { ...nextOwnProps, todos, addTodo } ownProps = nextOwnProps if (!shallowEqual(result, nextResult)) result = nextResult return result } } export default connectAdvanced(selectorFactory)(TodoApp)
讲了这么多,咱们看看connectAdvanced
是如何实现的,一开始原本想把全部的代码都列出来,可是感受直接列出200多行的代码看着确实不方便,因此咱们仍是一部分一部分介绍:
//代码总体结构 function connectAdvanced( selectorFactory, { getDisplayName = name => `ConnectAdvanced(${name})`, methodName = 'connectAdvanced', renderCountProp = undefined, shouldHandleStateChanges = true, storeKey = 'store', withRef = false, ...connectOptions } = {} ) { return function wrapWithConnect(WrappedComponent) { class Connect extends Component { //...... return hoistStatics(Connect, WrappedComponent) } }
函数接受两个参数:selectorFactory
与connectOptions
(可选),返回一个高阶组件wrapWithConnect
(以属性代理方式实现),高阶组件中建立了组件类Connect
, 最后返回了hoistStatics(Connect, WrappedComponent)
。其中hoistStatics
来源于:
import hoistStatics from 'hoist-non-react-statics'
做用是将WrappedComponent
中的非React特定的静态属性(例如propTypes
就是React的特定静态属性)赋值到Connect
。做用有点相似于Object.assign
,可是仅复制非React特定的静态属性。
其实对于React-Redux之因此可使得Provider
中的任何子组件访问到Redux中的store
并订阅store
,无非是利用context
,使得全部子组件都能访问store
。更进一步,咱们看看高阶组件时如何实现:
let hotReloadingVersion = 0 const dummyState = {} function noop() {} function connectAdvanced( selectorFactory, { getDisplayName = name => `ConnectAdvanced(${name})`, methodName = 'connectAdvanced', renderCountProp = undefined, shouldHandleStateChanges = true, storeKey = 'store', withRef = false, ...connectOptions } = {} ) { const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ const contextTypes = { [storeKey]: storeShape, [subscriptionKey]: subscriptionShape, } const childContextTypes = { [subscriptionKey]: subscriptionShape, } return function wrapWithConnect(WrappedComponent) { const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component' const displayName = getDisplayName(wrappedComponentName) const selectorFactoryOptions = { ...connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, withRef, displayName, wrappedComponentName, WrappedComponent } class Connect extends Component { } return hoistStatics(Connect, WrappedComponent) } }
上面的代码并无什么难以理解的,connectAdvanced
中定义了subscriptionKey
、version
以及为Connect
组件定义的contextTypes
与childContextTypes
(不了解context
的同窗能够看这里)。在高阶组件中所做的就是定义组装了selectorFactory
所用到的参数selectorFactoryOptions
。接下来介绍最重要的组件类Connect
:
class Connect extends Component { constructor(props, context) { super(props, context) this.version = version this.state = {} this.renderCount = 0 this.store = props[storeKey] || context[storeKey] this.propsMode = Boolean(props[storeKey]) this.setWrappedInstance = this.setWrappedInstance.bind(this) this.initSelector() this.initSubscription() } getChildContext() { const subscription = this.propsMode ? null : this.subscription return { [subscriptionKey]: subscription || this.context[subscriptionKey] } } 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() { return this.selector.shouldComponentUpdate } componentWillUnmount() { if (this.subscription) this.subscription.tryUnsubscribe() this.subscription = null this.notifyNestedSubs = noop this.store = null this.selector.run = noop this.selector.shouldComponentUpdate = false } getWrappedInstance() { return this.wrappedInstance } setWrappedInstance(ref) { this.wrappedInstance = ref } initSelector() { const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) this.selector = makeSelectorStateful(sourceSelector, this.store) this.selector.run(this.props) } initSubscription() { if (!shouldHandleStateChanges) return const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey] this.subscription = new Subscription(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.notifyNestedSubsOnComponentDidUpdate this.setState(dummyState) } } notifyNestedSubsOnComponentDidUpdate() { this.componentDidUpdate = undefined this.notifyNestedSubs() } isSubscribed() { return Boolean(this.subscription) && this.subscription.isSubscribed() } addExtraProps(props) { if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props const withExtras = { ...props } if (withRef) withExtras.ref = this.setWrappedInstance if (renderCountProp) withExtras[renderCountProp] = this.renderCount++ if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription 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() } } }
咱们首先来看构造函数:
constructor(props, context) { super(props, context) this.version = version this.state = {} this.renderCount = 0 this.store = props[storeKey] || context[storeKey] this.propsMode = Boolean(props[storeKey]) this.setWrappedInstance = this.setWrappedInstance.bind(this) this.initSelector() this.initSubscription() }
首先咱们先看看用来初始化selector
的initSelector
函数:
//Connect类方法 initSelector() { const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) this.selector = makeSelectorStateful(sourceSelector, this.store) this.selector.run(this.props) } //connectAdvanced外定义的函数 function makeSelectorStateful(sourceSelector, store) { // wrap the selector in an object that tracks its results between runs. const selector = { run: function runComponentSelector(props) { try { const nextProps = sourceSelector(store.getState(), props) if (nextProps !== selector.props || selector.error) { selector.shouldComponentUpdate = true selector.props = nextProps selector.error = null } } catch (error) { selector.shouldComponentUpdate = true selector.error = error } } } return selector }
咱们知道,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
类:
// 为链接到redux的store的组件以及嵌套的后代组件封装订阅逻辑,以确保祖先组件在后代组件以前刷新 const CLEARED = null const nullListeners = { notify() {} } function createListenerCollection() { //代码逻辑来源与store中 let current = [] let next = [] return { clear() { next = CLEARED current = CLEARED }, notify() { const listeners = current = next for (let i = 0; i < listeners.length; i++) { listeners[i]() } }, subscribe(listener) { let isSubscribed = true if (next === current) next = current.slice() next.push(listener) return function unsubscribe() { if (!isSubscribed || current === CLEARED) return isSubscribed = false if (next === current) next = current.slice() next.splice(next.indexOf(listener), 1) } } } } export default class Subscription { constructor(store, parentSub, onStateChange) { this.store = store this.parentSub = parentSub this.onStateChange = onStateChange this.unsubscribe = null this.listeners = nullListeners } addNestedSub(listener) { this.trySubscribe() return this.listeners.subscribe(listener) } notifyNestedSubs() { this.listeners.notify() } isSubscribed() { return Boolean(this.unsubscribe) } trySubscribe() { if (!this.unsubscribe) { this.unsubscribe = this.parentSub ? this.parentSub.addNestedSub(this.onStateChange) : this.store.subscribe(this.onStateChange) this.listeners = createListenerCollection() } } tryUnsubscribe() { if (this.unsubscribe) { this.unsubscribe() this.unsubscribe = null this.listeners.clear() this.listeners = nullListeners } } }
首先咱们先看函数createListenerCollection
,这边的代码逻辑和redux
中的listener
逻辑一致,能够了解一下以前的文章Redux:百行代码千行文档。createListenerCollection
经过闭包的方式存储current
和next
,而后返回
{ clear, notify, subscribe }
做为对外接口,分别用来清除当前存储的listener、通知、订阅,其目的就是实现一个监听者模式。而后类Subscription
封装了订阅的逻辑,Subscription
根据构造函数中是否传入了父级的订阅类Subscription实例parentSub
,订阅方法trySubscribe
会有不一样的行为。首先看看parentSub
的来源:
//this.propsMode来自于constructor中的this.propsMode = Boolean(props[storeKey]),storeKey默认为`store` const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
咱们知道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
方法中的
this.store.subscribe(this.onStateChange)。
状况2:
若是当前组件并非通往根节点的路径中第一个链接到Redux的store的组件,也就是父组件中存在已经链接到Redux的store的组件。这时候,必需要保证下层的组件响应store
改变的函数调用必须晚于父级组件响应store
的函数调用,例如在图中红色的组件在store更新时是晚于黑色的组件的。代码中是以下实现的,在父级组件中,以下:
getChildContext() { const subscription = this.propsMode ? null : this.subscription return { [subscriptionKey]: subscription || this.context[subscriptionKey] } }
所以在子组件(红色)中就能够经过context
得到父组件的subscription(也就是parentSub)。这样在执行tryUnsubscribe
时对应于
this.parentSub.addNestedSub(this.onStateChange)
这样咱们将子组件处理store中state的函数添加到parentSub
中的listener
中。这样在父组件更新结束后,就能够调用this.notifyNestedSubs()
。这样就保证了更新顺序,子组件永远在父组件更新以后。
状况3:
如上图所示,右边的组件是经过属性prop的方式传入了store
,那么组件中的this.store
中的值就是经过以props传入的store
。假如祖先元素没有链接到store
的组件,那么当前组件中parentSub
值就为空。因此订阅的方式就是以props中的store
:
this.store.subscribe(this.onStateChange)。
状况4:
如上图所示,右下方的组件的父组件(紫色)是经过props传入store
的,那么在父组件(紫色)中有
getChildContext() { const subscription = this.propsMode ? null : this.subscription return { [subscriptionKey]: subscription || this.context[subscriptionKey] } }
父组件对子组件暴露context
,其中context
中的subscriptionKey
属性值为this.context[subscriptionKey]
,要么是null
,要么是祖先元素中非props
方式传入store
的组件的subscription
。也就是说以props传入的store
的父组件不会影响子组件的订阅store
。感受说的太过于抽象,咱们举个例子:
在上面这个例子中,若是发出dispatch
更新store1,组件A和组件C都会刷新,组件B不会刷新。
讨论了这么多,咱们能够看一下initSubscription
的实现方式:
initSubscription() { if (!shouldHandleStateChanges) return const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey] this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this)) this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription) }
若是当前的store不是以props的方式传入的,那么parentSub
是this.context[subscriptionKey]
。若是是以props的方式传入的,若显式地给组件以props的方式传入subscription时,parentSub
值为this.props.subscription
。须要注意的是,咱们在initSubscription
中拷贝了当前this.subscription
中的notifyNestedSubs
方法,目的是防止在notify
循环过程当中组件卸载,使得this.subscription
为null
。咱们在组件卸载时,会将值赋值为一个名为no-loop
的空函数,避免出错。固然这并非惟一的解决方法。
接下咱们能够看一下Connect
组件中主要生命周期函数:
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() { return this.selector.shouldComponentUpdate } componentWillUnmount() { if (this.subscription) this.subscription.tryUnsubscribe() this.subscription = null this.notifyNestedSubs = noop this.store = null this.selector.run = noop this.selector.shouldComponentUpdate = false }
组件在did mount
时会根据可选参数shouldHandleStateChanges
选择是否订阅store
的state
改变。组件在接受props时,会使用selector计算新的props并执行相应的声明周期。shouldComponentUpdate
会根据this.selector
存储的值shouldComponentUpdate
来判断是否须要刷新组件。在组件will mount
时会作相应的清理,防止内存泄露。
接着咱们介绍其余的类方法:
getWrappedInstance() { return this.wrappedInstance } setWrappedInstance(ref) { this.wrappedInstance = ref }
getWrappedInstance
与setWrappedInstance
在可选参数withRef
为true时,获取或者存储被包裹组件的实例(ref)。
onStateChange() { this.selector.run(this.props) if (!this.selector.shouldComponentUpdate) { this.notifyNestedSubs() } else { this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate this.setState(dummyState)//dummyState === {} } } notifyNestedSubsOnComponentDidUpdate() { this.componentDidUpdate = undefined this.notifyNestedSubs() }
onStateChange
函数是store发生改变的回调函数,当回调onStateChange
方法时,会经过selector计算新的props,若是计算selcetor的结果中shouldComponentUpdate
为false
,表示不须要刷新当前组件仅须要通知子组件更新。若是shouldComponentUpdate
为true
,会经过设置this.setState({})
来刷新组件,并使得在组件更新结束以后,通知子组件更新。
addExtraProps(props) { if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props const withExtras = { ...props } if (withRef) withExtras.ref = this.setWrappedInstance if (renderCountProp) withExtras[renderCountProp] = this.renderCount++ if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription return withExtras }
addExtraProps
函数主要用做为selector
计算出的props
增长新的属性。例如,ref
属性用来绑定回调存储组件实例的函数setWrappedInstance
,renderCountProp
为当前组件属性刷新的次数,subscriptionKey
用来传递当前connect
中的subscription
。
render() { const selector = this.selector selector.shouldComponentUpdate = false if (selector.error) { throw selector.error } else { return createElement(WrappedComponent, this.addExtraProps(selector.props)) } }
render
函数其实就是高阶函数中的属性代理,首先将shouldComponentUpdate
置回false
,而后根据selector
中的计算过程是否存在error
,若是存在error
就抛出,不然执行
createElement(WrappedComponent, this.addExtraProps(selector.props))
若是你对上面语句不太熟悉,其实上面代码等同于:
return ( <WrappedComponent {...this.addExtraProps(selector.props)} /> )
其实所谓的jsx
也无非是createElement
语法糖,全部的jsx
的语法都会被编译成React.createElement
,因此哪怕你的代码中没有显式的用到React
,只要有jsx
语法,就必须存在React
。
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() } } }
React-Redux在生产环境下是不支持热重载的,只有在开发环境下提供这个功能。在开发环境中,组件在will update
时会根据this.version
与version
去判断,若是二者不同,则初始化selector
,取消以前的订阅并从新订阅新的subscription
。
import { Component, Children } from 'react' import PropTypes from 'prop-types' import { storeShape, subscriptionShape } from '../utils/PropTypes' import warning from '../utils/warning' 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.' ) } export function createProvider(storeKey = 'store', subKey) { const subscriptionKey = subKey || `${storeKey}Subscription` class Provider extends Component { getChildContext() { return { [storeKey]: this[storeKey], [subscriptionKey]: null } } constructor(props, context) { super(props, context) this[storeKey] = props.store; } render() { return Children.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' return Provider } export default createProvider()
首先咱们看看函数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
仅支持单个子组件。
最后欢迎你们关注个人掘金帐号或者博客,不足之处,欢迎指正。