动手实现 React-redux(三):connect 和 mapStateToProps

咱们来观察一下刚写下的这几个组件,能够轻易地发现它们有两个重大的问题:html

  1. 有大量重复的逻辑:它们基本的逻辑都是,取出 context,取出里面的 store,而后用里面的状态设置本身的状态,这些代码逻辑其实都是相同的。
  2. 对 context 依赖性过强:这些组件都要依赖 context 来取数据,使得这个组件复用性基本为零。想一下,若是别人须要用到里面的 ThemeSwitch 组件,可是他们的组件树并无 context 也没有 store,他们无法用这个组件了。

对于第一个问题,咱们在 高阶组件 的章节说过,能够把一些可复用的逻辑放在高阶组件当中,高阶组件包装的新组件和原来组件之间经过 props 传递信息,减小代码的重复程度。react

对于第二个问题,咱们得弄清楚一件事情,到底什么样的组件才叫复用性强的组件。若是一个组件对外界的依赖过于强,那么这个组件的移植性会不好,就像这些严重依赖 context 的组件同样。编程

若是一个组件的渲染只依赖于外界传进去的 props 和本身的 state,而并不依赖于其余的外界的任何数据,也就是说像纯函数同样,给它什么,它就吐出(渲染)什么出来。这种组件的复用性是最强的,别人使用的时候根本不用担忧任何事情,只要看看 PropTypes 它能接受什么参数,而后把参数传进去控制它就好了。redux

咱们把这种组件叫作 Pure Component,由于它就像纯函数同样,可预测性很是强,对参数(props)之外的数据零依赖,也不产生反作用。这种组件也叫 Dumb Component,由于它们呆呆的,让它干啥就干啥。写组件的时候尽可能写 Dumb Component 会提升咱们的组件的可复用性。app

到这里思路慢慢地变得清晰了,咱们须要高阶组件帮助咱们从 context 取数据,咱们也须要写 Dumb 组件帮助咱们提升组件的复用性。因此咱们尽可能多地写 Dumb 组件,而后用高阶组件把它们包装一层,高阶组件和 context 打交道,把里面数据取出来经过 props 传给 Dumb 组件。函数

咱们把这个高阶组件起名字叫 connect,由于它把 Dumb 组件和 context 链接(connect)起来了:this

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export connect = (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    // TODO: 如何从 store 取数据?

    render () {
      return <WrappedComponent />
    }
  }

  return Connect
}

connect 函数接受一个组件 WrappedComponent 做为参数,把这个组件包含在一个新的组件 Connect 里面,Connect 会去 context 里面取出 store。如今要把 store 里面的数据取出来经过 props 传给 WrappedComponentspa

可是每一个传进去的组件须要 store 里面的数据都不同的,因此除了给高阶组件传入 Dumb 组件之外,还须要告诉高级组件咱们须要什么数据,高阶组件才能正确地去取数据。为了解决这个问题,咱们能够给高阶组件传入相似下面这样的函数:设计

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor,
    themeName: state.themeName,
    fullName: `${state.firstName} ${state.lastName}`
    ...
  }
}

这个函数会接受 store.getState() 的结果做为参数,而后返回一个对象,这个对象是根据 state 生成的。mapStateTopProps 至关于告知了 Connect 应该如何去 store 里面取数据,而后能够把这个函数的返回结果传给被包装的组件:code

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    render () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState())
      // {...stateProps} 意思是把这个对象里面的属性所有经过 `props` 方式传递进去
      return <WrappedComponent {...stateProps} />
    }
  }

  return Connect
}

connect 如今是接受一个参数 mapStateToProps,而后返回一个函数,这个返回的函数才是高阶组件。它会接受一个组件做为参数,而后用 Connect 把组件包装之后再返回。 connect 的用法是:

...
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)
...

有些朋友可能会问为何不直接 const connect = (mapStateToProps, WrappedComponent),而是要额外返回一个函数。这是由于 React-redux 就是这么设计的,而我的观点认为这是一个 React-redux 设计上的缺陷,这里有机会会在关于函数编程的章节再给你们科普,这里暂时不深究了。

咱们把上面 connect 的函数代码单独分离到一个模块当中,在 src/ 目录下新建一个 react-redux.js,把上面的 connect 函数的代码复制进去,而后就能够在 src/Header.js 里面使用了:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from './react-redux'

class Header extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render () {
    return (
      <h1 style={{ color: this.props.themeColor }}>React.js 小书</h1>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)

export default Header

能够看到 Header 删掉了大部分关于 context 的代码,它除了 props 什么也不依赖,它是一个 Pure Component,而后经过 connect 取得数据。咱们不须要知道 connect 是怎么和 context 打交道的,只要传一个 mapStateToProps 告诉它应该怎么取数据就能够了。一样的方式修改 src/Content.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
import { connect } from './react-redux'

class Content extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render () {
    return (
      <div>
        <p style={{ color: this.props.themeColor }}>React.js 小书内容</p>
        <ThemeSwitch />
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Content = connect(mapStateToProps)(Content)

export default Content

connect 尚未监听数据变化而后从新渲染,因此如今点击按钮只有按钮会变颜色。咱们给 connect 的高阶组件增长监听数据变化从新渲染的逻辑,稍微重构一下 connect

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = { allProps: {} }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState(), this.props) // 额外传入 props,让获取数据更加灵活方便
      this.setState({
        allProps: { // 整合普通的 props 和从 state 生成的 props
          ...stateProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }

  return Connect
}

咱们在 Connect 组件的 constructor 里面初始化了 state.allProps,它是一个对象,用来保存须要传给被包装组件的全部的参数。生命周期 componentWillMount 会调用调用 _updateProps 进行初始化,而后经过 store.subscribe 监听数据变化从新调用 _updateProps

为了让 connect 返回新组件和被包装的组件使用参数保持一致,咱们会把全部传给 Connect 的 props 原封不动地传给 WrappedComponent。因此在 _updateProps 里面会把 stateProps 和 this.props 合并到 this.state.allProps 里面,再经过 render 方法把全部参数都传给 WrappedComponent

mapStateToProps 也发生点变化,它如今能够接受两个参数了,咱们会把传给 Connect 组件的 props 参数也传给它,那么它生成的对象配置性就更强了,咱们能够根据 store 里面的 state 和外界传入的 props 生成咱们想传给被包装组件的参数。

如今已经很不错了,Header.js 和 Content.js 的代码都大大减小了,而且这两个组件 connect 以前都是 Dumb 组件。接下来会继续重构 ThemeSwitch

相关文章
相关标签/搜索