redux真的不复杂——第二篇:react-redux源码分析

第一篇连接: redux真的不复杂——源码解读javascript

预备知识:1. 了解redux的基本使用; 2. Context APIhtml

了解redux原理更好,若是不了解,不妨先看看第一篇博客。java

redux是一个状态管理的工具,本质上是一个js对象(包含状态,以及一些处理状态的方法)。react

因此redux具备很强的适应性,能够配合其余工具/框架一块儿使用。git

react-redux则是一个让你更容易地在react中使用redux的工具。github

为何须要redux

咱们使用redux的目的是存储状态,在react中有存储状态的东西吗?redux

有,state。可是state有一些局限性。api

对于一个组件来讲,(用setState触发)state的变化会触发这个组件以及它全部子组件的更新,因此为了优化考虑,咱们每每将state放在更“局部”的组件中,这样state的变化只会引发最少的(有必要的)更新。缓存

那么若是咱们的react应用有一些状态在多个地方均可能用到,特别是一些全局数据(好比当前用户信息,全局的通知等等),对于这些数据咱们有两个选择:app

  1. 在各个局部组件中各存一份

    优势是:保证了数据的变化只会引发最小的组件更新,

    缺点是

    • 难以保证各处的数据同步
    • 可能各处会重复请求相同的API,损失了必定性能
  2. 在全局组件中存一份

    优势是:只须要在一个地方请求API,数据是彻底同步的

    缺点是:数据的变化会引发整个应用大量的更新。

你会发现这两个选择各有优缺点,但仔细想一想,你会发现其实咱们有第三个选择:

利用Context API。在全局组件外包一个Provider,将数据存在Provider上。子组件经过访问context来使用这些数据

这样就兼顾了各个优势:

  • 数据易同步
  • 只需在一个地方请求API
  • 全局数据的更新不会引发组件的更新(Context api的特性)

一切仿佛变得美好了,不是吗?

可是问题又来了:

  • 全局(Provider上)的数据你如何管理(特别是当数据的结构复杂了以后)?
  • 子组件如何根据须要更新这些数据?

这个时候你就会想到redux了,redux提供了一套优雅的管理状态的方案。

优雅在什么地方?接着往下看。

为何须要react-redux

想要使用context api的方案,而且还要使用redux,那么你须要作的事情有:

  1. 用redux建立一个store
  2. 在react应用的最外层包一个Provider,store放在Provider上。
  3. 子组件想要获取数据时:在组件外包一个Consumer,Consumer获取到store,传递给组件。
  4. 当子组件想要更新数据时:调用store的dispatch方法触发store的更新。

差很少就这些,是否是也挺简单?

可是从技术上来说,你须要作的事情有:

  1. 本身写一个provider
  2. 每次想要使用provider的数据的时候,本身写一个Consumer组件
  3. 每次想要更新数据的时候,本身调用store的diapatch方法。

难受吗?

你须要react-redux,它帮你把这些操做都封装了起来。

react-redux源码分析

为了源码更清晰,分析时只展现了一些核心代码,省略了错误处理,通用性处理等代码。建议你参照着真正的源码阅读。

react-redux的使用

在看源码以前,先简单回忆一下react-redux的用法:

  1. 从react-redux库引入一个Provider组件,将redux建立的store做为属性传递给这个Provider组件。
  2. 给connect传递mapStateToPropsmapDispatchToProps两个参数(还有其余可选参数mergeProps,options),获得一个高阶组件【注1】。
  3. 用高阶组件“包装”咱们本身的组件,就能在本身的组件中获得对应的props。

【注1】高阶组件:输入为组件,输出为另外一个组件的函数

ok,咱们来看看源码的结构:

还挺复杂,不过不要紧,看看index.js:

//index.js

import Provider, { createProvider } from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import connect from './connect/connect'

export { Provider, createProvider, connectAdvanced, connect }
复制代码

createProvider和connectAdvanced这两个方法是定制react-redux的工做方式时使用的,咱们通常不会直接用到,因此咱们就从Provider和connect这两个模块入手。

Provider

Provider.js的源码比较少,结构也很简单:

//Provider.js

//import略
//输出建立Provider的函数
export function createProvider(storeKey = 'store'){
    class Provider extends Component {
        // ...
    }
    
    return Provider
}

//默认输出建立后的Provider
export default createProvider()
复制代码

这个文件提供了两个输出,一个是建立Provider的函数,还有一个是建立过的Provider。

下面咱们来详细看看Provider这个组件具体是如何实现的:

class Provider extends Component {
    // 访问context的钩子函数【注2】
    getChildContext() {
        // 返回一个对象,键是store的标识(可自定义),值是store
        return { [storeKey]: this[storeKey] }
    }
    
    constructor(props, context) {
        super(props, context)
        // 将咱们(经过props)传进来的store存在本身的实例中。
        this[storeKey] = props.store
    }
    
    render() {
        // Children.only是react提供的API函数,
        // 做用是限制this.props.children只能是一个React元素,不然会报错
        return Children.only(this.props.children)
    }
}

Provider.childContextTypes = {
    // ...
}
复制代码

看到这里,能够发现Provider的实现很是简单,只是将咱们(经过props)传进去的store,建立了一个context而已。

注2:React能够经过在一个组件中设置getChildContextchildContextTypes来建立context,在其子组件中设置contextTypes属性来接收context

原来,react-redux只是将store放到Provider组件的context上。那么问题来了,

问题1

Provider的子组件如何使用store?


Connect

顾名思义,connect函数的做用是——”链接“,将一个正常的组件与咱们的store链接起来,这样子组件就能够“使用”store了。

然而其实是如何实现链接的呢?若是你使用过react-redux的话,你就知道是:

  1. 使用connect建立一个高阶组件
  2. 而后用高阶组件包裹一个组件,向组件传递额外的props
  3. 组件内部经过props,可以:
    • 读取store
    • 触发(dispatch)store中的action。

咱们使用connect建立高阶组件时一般会传入mapStateToPropsmapDispatchToProps,然而高阶组件并无直接将其经过props传进被包裹组件——而是通过筛选和包装后,再经过props传入一些东西(store的一部分分支,或者自动dispatch的action creator)。

回答问题1(Provider的子组件如何使用store?):

经过将“筛选和包装”后的与store相关的东西,经过props传入组件,来使用store

可见,筛选和包装是一个很是重要的任务,那么它是在connect中实现的吗?

咱们来看看connect的源码:

//connect.js

export function createConnect({ connectHOC = connectAdvanced, // 记住这个函数,后面会讲 // 选择器工厂:根据一些配置生成一个选择器 // (选择器工厂的实现,以及选择器的做用后面咱们会讲) selectorFactory = defaultSelectorFactory // ... 一些处理connect参数的工厂函数 } = {}) {
    // 这里才是connect函数
    return function connect({ mapStateToProps, mapDispatchToProps, mergeProps, options={} }){ 
        // connect的参数有:mapStateToProps,mapDispatchToProps,mergeProps,options
        // 然而这些参数并不能直接使用,它们多是对象,也多是函数
        // 因此在这里进行通用性处理,是他们能够直接使用
        
        // connect返回了一个高阶组件(由connectHOC建立)
        return connectHOC(selectorFactory, { /*配置对象*/ })
    }
}

// 默认输出的connect
export default createConnect()
复制代码

彷佛connect并无作筛选和包装这件事,仅仅返回了一个高阶组件,而这个高阶组件默认是由connectAdvanced建立的。

因此也就是说:

connect只是connectAdvanced(这个函数一会再看)的一个包装,connect自己只是一个预处理函数,真正的“筛选和包装”实际上是在connectAdvanced这个函数里进行。

connect作的事情仅仅是:

选择器工厂配置对象传给connectAdvanced进行进一步处理。

(彷佛“筛选和包装”的功能是经过选择器工厂和配置对象实现的,下面咱们来看看是否是如此。)

问题2

“筛选和包装”的功能是如何实现的?


connectAdvanced

从上面的代码能够看出,connectAdvanced的做用是:

根据selectorFactory和配置对象,建立一个高阶组件。

下面看看源码:

function connectAdvanced(selectorFactory, { /*options配置对象*/ }) {
    // ... 根据options初始化一些内部变量
    
    //返回一个高阶组件(输入为一个组件,输出为另外一个组件的函数)
    return function wrapWithConnect(WrappedComponent){
        // 高阶组件返回的组件Connect
        class Connect extends Component {
            constructor(props, context) {
                super(props, context)

                this.state = {}
                // 从props或context读取store
                //(由于WrappedComponent本身可能也是一个Provider)
                // 在本文的分析中咱们忽略Provider嵌套的这种状况
                this.store = props[storeKey] || context[storeKey]
                // ...
            	this.initSelector() // 初始化一个selector(后面会讲)
            }
            
            // 初始化selector的函数
            initSelector() {/*...*/}
            
            // ... 其余一些生命周期函数和工具函数
        }
        Connect.contextTypes = contextTypes // 获取外层的context
        Connect.propTypes = contextTypes
                            
        // 返回Connect组件的时候多了一步处理
        // 这个函数的做用是将WrappedComponent上的静态方法拷贝到Connect上,并返回Connect
        // 这样就能够彻底把Connect看成一个“WrappedComponent”使用
        return hoistNonReactStatics(Connect, WrappedComponent);
    }
}
复制代码

结构依旧很简单,就是返回一个高阶组件。所谓高阶组件,实际上是一个函数。

高阶组件接收被包裹的组件,返回一个Connect组件,下面咱们的重点就放在这个Connect组件是如何建立的。

constructor看起来很普通,只不过从context中获取了store,而后将store存下来。还调用了一个initSelector()函数,初始化选择器?选择器是什么东西???别急咱们一步一步来看。

看看其源码:

initSelector() {
    // 传入dispatch方法和配置对象,获得一个原始选择器
    const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
    // 对这个原始选择器进行进一步处理,获得一个最终的"stateful"的选择器selector
    this.selector = makeSelectorStateful(sourceSelector, this.store)
    // 调用selector的一个方法
    this.selector.run(this.props)
}
复制代码

看完难免又有疑惑了:

  • selectorFactory到底作了什么?
  • makeSelectorStateful作了什么?添加的run方法是作什么的?

一个一个来看。


1. selectorFactory----------------------------------
// ...

function impureFinalPropsSelectorFactory({ /*配置对象*/ }){
    // ...
}

function pureFinalPropsSelectorFactory({ /*配置对象*/ }){
    // ...
}

export default function finalPropsSelectorFactory(dispatch, { /*配置对象*/ }) {
  // 从配置对象拿到initMapStateToProps,initMapDispatchToProps,initMergeProps
  // 这些方法是对connect函数参数(mapStateToProps,mapDispatchToProps,mergeProps)的包装
  // 因此调用后返回的是加强后,能够直接使用的同名函数
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  // 根据options的pure字段肯定使用哪一种工厂函数
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 使用选择器工厂,返回一个选择器
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options //areStatesEqual, areOwnPropsEqual, areStatePropsEqual
  )
}
复制代码

咱们来看看两种工厂函数:

// pure为false时的选择器工厂
// 功能及其简单,每次调用都返回一个新组装的props
function impureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch ) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}

// pure为true时的选择器工厂
function pureFinalPropsSelectorFactory({ mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } }){
    let hasRunAtLeastOnce = false // 是不是第一次使用选择器
    let state // 这个state并非组件的状态,而是redux的store
    let ownProps // 存储上一次传入的ownProps
    let stateProps // 经过mapStateToProps筛选出的要放进props的数据
    let dispatchProps // 经过mapDispatchToProps筛选出的要放进props的数据
    let mergedProps // 合并后的props,最终选择器返回的就是这个参数。
    
    // 第一次调用选择器的函数
    function handleFirstCall(firstState, firstOwnProps){
        state = firstState
        ownProps = firstOwnProps
        stateProps = mapStateToProps(state, ownProps)
        dispatchProps = mapDispatchToProps(dispatch, ownProps)
        mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
        hasRunAtLeastOnce = true
        return mergedProps
    }
    
    // 处理不一样状况时返回什么props
    // 这三个函数也没什么稀奇的骚操做,就不展开了
    // 仅仅是根据须要调用mapStateToProps或mapDispatchToProps获得stateProps和dispatchProps
    // 而后调用mergeProps获得mergedProps
    function handleNewPropsAndNewState(){}
    function handleNewProps(){}
    function handleNewState(){}
    
    function handleSubsequentCalls(nextState, nextOwnProps) {
        // areOwnPropsEqual,areStatesEqual用于比较新旧state,props是否相同
        // 这是从配置对象中拿到的方法,实现方式就只是一个浅比较
        const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
        const stateChanged = !areStatesEqual(nextState, state)
        state = nextState
        ownProps = nextOwnProps

        if (propsChanged && stateChanged) return handleNewPropsAndNewState()
        if (propsChanged) return handleNewProps()
        if (stateChanged) return handleNewState()
        return mergedProps
    }
    
    // 这里是返回的选择器
    return function pureFinalPropsSelector(nextState, nextOwnProps) {
        return hasRunAtLeastOnce // 判断是不是第一次使用选择器
        	? handleSubsequentCalls(nextState, nextOwnProps)
        	: handleFirstCall(nextState, nextOwnProps) 
    }
}
复制代码

能够看出选择器工厂返回了一个函数pureFinalPropsSelector,这就一个选择器。

能够看出,选择器的功能,就是接收nextStatenextOwnProps,返回一个通过“筛选和包装”的props。返回的props能够直接传给被包裹的组件。

回答问题2(“筛选和包装”的功能是如何实现的?):

使用选择器工厂,根据咱们传进去的配置项(通过处理的mapXXXToProps,dispatch,浅比较方法),生成一个具备“筛选和包装”功能的选择器

connectAvanced中使用的selectorFactory已经弄明白了,下面看看另外一个makeSelectorStateful函数。

2. makeSelectorStateful---------------------------------
function makeSelectorStateful(sourceSelector, store) {
  // 建立了一个selector对象,这个对象有一个run方法
  const selector = {
    // run方法接收原始props(外部传给被包裹组件的props),
    // 而且调用了一次原始选择器,获得调用后的props,
    // 将新props和内部缓存的旧props比较,
    // 根据结果,设置selector的shouldComponentUpdate属性。
    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
}
复制代码

如今再回到connectAdvanced的源码:

function connectAdvanced(selectorFactory, { /*options配置对象*/ }) {
    //返回一个高阶组件(输入为一个组件,输出为另外一个组件的函数)
    return function wrapWithConnect(WrappedComponent){
        // 高阶组件返回的组件Connect
        class Connect extends Component {
            constructor(props, context) {
                super(props, context)

                this.state = {}
                // 从props或context读取store
                this.store = props[storeKey] || context[storeKey]
            	this.initSelector() // 初始化一个选择器
            }
            
            initSelector() {/*...*/}
            
            // 咱们如今要重点看这里!!!!!!!!
            // ... 其余一些生命周期函数和工具函数
        }
        // ...
        return hoistNonReactStatics(Connect, WrappedComponent);
    }
}
复制代码

咱们已经知道选择器的功能是:获取“筛选和包装”后的props,如今咱们看看Connect组件是如何使用选择器的。将关注点放在Connect这个组件的生命周期函数是如何使用的:

class Connect extends Component {
    constructor(props, context) {
        // ...
        this.initSelector()
    }
            
    
    componentDidMount() {
        // 向store中添加监听器,监听器函数在下面
        // 就不详细看这个函数的实现了,就是简单的调用store的subscribe方法
        this.subscription.trySubscribe() 
        
        // 运行选择器,根据选择器运行后的结果判断是否须要更新
        this.selector.run(this.props)
        if (this.selector.shouldComponentUpdate) this.forceUpdate()
    }
    // 当接收新的props时,运行选择器
    componentWillReceiveProps(nextProps) {
        this.selector.run(nextProps)
    }
    // 根据选择器运行后的结果判断是否须要更新
    shouldComponentUpdate() {
        return this.selector.shouldComponentUpdate
    }
    // 卸载组件时清理内存
    componentWillUnmount() {
        // 卸载监听器
        if (this.subscription) this.subscription.tryUnsubscribe()
        this.store = null
        this.selector.run = noop // 空函数function noop() {}
        this.selector.shouldComponentUpdate = false
    }
    
    // 监听器,将会用subscribe方法添加到store上,每当store被dispatch会被调用
    onStateChange() {
        this.selector.run(this.props)
    }
    
    render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          // addExtraProps方法的做用时将selector筛选后的props,添加到本来的props上。
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }
   }
}
复制代码

原来这么简单啊,就只是:

  • 在须要的时候:

    • Connect组件第一次装载组件时
    • Connect组件接收props时
    • 监听store的监听器被触发时

    运行选择器,获得须要添加的额外的props

  • 根据运行的结果肯定是否更新Connect组件

  • 渲染时向被包裹组件添加额外的props。

总结

若是你看到了最后,你会发现,react-redux的实现方式和咱们文章开头的解决方案一毛同样:

  • store存在父组件的context上
  • 给子组件添加额外的props,以实现和store的交互

实现的亮点在于,react-redux用了高阶组件这种优雅的方式,将这种需求进行了封装。

若是有疑问,或者想要交流的地方,欢迎在评论区讨论。

相关文章
相关标签/搜索